Merge pull request #321 from simPod/validator-cs

Fix CS in Validator folder
This commit is contained in:
Vladimir Razuvaev 2018-08-12 01:08:05 +07:00 committed by GitHub
commit cc39b3ecbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 2129 additions and 1710 deletions

View File

@ -1,6 +1,8 @@
# Changelog
## dev-master
- Spec compliance: error extensions are displayed under `extensions` key
- `AbstractValidationRule` renamed to `ValidationRule` (NS `GraphQL\Validator\Rules`)
- `AbstractQuerySecurity` renamed to `QuerySecurityRule` (NS `GraphQL\Validator\Rules`)
#### v0.12.5
- Execution performance optimization for lists

View File

@ -119,7 +119,7 @@ static function getStandardTypes()
* Returns standard validation rules implementing GraphQL spec
*
* @api
* @return AbstractValidationRule[]
* @return ValidationRule[]
*/
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
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)).
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
* @param Schema $schema
* @param DocumentNode $ast
* @param AbstractValidationRule[]|null $rules
* @param ValidationRule[]|null $rules
* @param TypeInfo|null $typeInfo
* @return Error[]
*/
@ -1275,7 +1275,7 @@ static function validate(
* Returns all global validation rules.
*
* @api
* @return AbstractValidationRule[]
* @return ValidationRule[]
*/
static function allRules()
```
@ -1289,7 +1289,7 @@ static function allRules()
*
* @api
* @param string $name
* @return AbstractValidationRule
* @return ValidationRule
*/
static function getRule($name)
```
@ -1299,9 +1299,9 @@ static function getRule($name)
* Add rule to list of global validation rules
*
* @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
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\SourceLocation;
use GraphQL\Utils\Utils;
use Traversable;
use function array_filter;
use function array_map;
use function array_merge;
use function is_array;
use function iterator_to_array;
@ -82,7 +82,7 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
/**
* @param string $message
* @param Node[]|null $nodes
* @param Node|Node[]|Traversable|null $nodes
* @param mixed[]|null $positions
* @param mixed[]|null $path
* @param \Throwable $previous

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,13 @@
<?php
declare(strict_types=1);
namespace GraphQL\Language\AST;
interface SelectionNode
{
/**
/**
* 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\Type\Schema;
use GraphQL\Utils\Utils;
use GraphQL\Validator\Rules\AbstractValidationRule;
use GraphQL\Validator\Rules\ValidationRule;
use function is_array;
use function is_callable;
use function method_exists;
@ -73,7 +73,7 @@ class ServerConfig
/** @var bool */
private $queryBatching = false;
/** @var AbstractValidationRule[]|callable */
/** @var ValidationRule[]|callable */
private $validationRules;
/** @var callable */
@ -150,7 +150,7 @@ class ServerConfig
* Set validation rules for this server.
*
* @api
* @param AbstractValidationRule[]|callable $validationRules
* @param ValidationRule[]|callable $validationRules
* @return self
*/
public function setValidationRules($validationRules)
@ -281,7 +281,7 @@ class ServerConfig
}
/**
* @return AbstractValidationRule[]|callable
* @return ValidationRule[]|callable
*/
public function getValidationRules()
{

View File

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

View File

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

View File

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

View File

@ -1,14 +1,15 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator;
use GraphQL\Error\Error;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\Visitor;
use GraphQL\Type\Schema;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use GraphQL\Utils\TypeInfo;
use GraphQL\Validator\Rules\AbstractValidationRule;
use GraphQL\Validator\Rules\ValuesOfCorrectType;
use GraphQL\Validator\Rules\DisableIntrospection;
use GraphQL\Validator\Rules\ExecutableDefinitions;
use GraphQL\Validator\Rules\FieldsOnCorrectType;
@ -27,6 +28,7 @@ use GraphQL\Validator\Rules\PossibleFragmentSpreads;
use GraphQL\Validator\Rules\ProvidedNonNullArguments;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\QueryDepth;
use GraphQL\Validator\Rules\QuerySecurityRule;
use GraphQL\Validator\Rules\ScalarLeafs;
use GraphQL\Validator\Rules\UniqueArgumentNames;
use GraphQL\Validator\Rules\UniqueDirectivesPerLocation;
@ -34,9 +36,16 @@ use GraphQL\Validator\Rules\UniqueFragmentNames;
use GraphQL\Validator\Rules\UniqueInputFieldNames;
use GraphQL\Validator\Rules\UniqueOperationNames;
use GraphQL\Validator\Rules\UniqueVariableNames;
use GraphQL\Validator\Rules\ValidationRule;
use GraphQL\Validator\Rules\ValuesOfCorrectType;
use GraphQL\Validator\Rules\VariablesAreInputTypes;
use GraphQL\Validator\Rules\VariablesDefaultValueAllowed;
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.
@ -47,7 +56,7 @@ use GraphQL\Validator\Rules\VariablesInAllowedPosition;
* 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.
*
* 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)).
*
* Visitor methods are expected to return an instance of [GraphQL\Error\Error](reference.md#graphqlerrorerror),
@ -58,55 +67,54 @@ use GraphQL\Validator\Rules\VariablesInAllowedPosition;
*/
class DocumentValidator
{
/** @var ValidationRule[] */
private static $rules = [];
/** @var ValidationRule[]|null */
private static $defaultRules;
/** @var QuerySecurityRule[]|null */
private static $securityRules;
/** @var bool */
private static $initRules = false;
/**
* Primary method for query validation. See class description for details.
*
* @api
* @param Schema $schema
* @param DocumentNode $ast
* @param AbstractValidationRule[]|null $rules
* @param TypeInfo|null $typeInfo
* @param ValidationRule[]|null $rules
* @return Error[]
*/
public static function validate(
Schema $schema,
DocumentNode $ast,
array $rules = null,
TypeInfo $typeInfo = null
)
{
if (null === $rules) {
?array $rules = null,
?TypeInfo $typeInfo = null
) {
if ($rules === null) {
$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
return [];
}
$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.
*
* @api
* @return AbstractValidationRule[]
* @return ValidationRule[]
*/
public static function allRules()
{
if (!self::$initRules) {
if (! self::$initRules) {
static::$rules = array_merge(static::defaultRules(), self::securityRules(), self::$rules);
static::$initRules = true;
}
@ -116,7 +124,7 @@ class DocumentValidator
public static function defaultRules()
{
if (null === self::$defaultRules) {
if (self::$defaultRules === null) {
self::$defaultRules = [
ExecutableDefinitions::class => new ExecutableDefinitions(),
UniqueOperationNames::class => new UniqueOperationNames(),
@ -151,7 +159,7 @@ class DocumentValidator
}
/**
* @return array
* @return QuerySecurityRule[]
*/
public static function securityRules()
{
@ -159,16 +167,36 @@ class DocumentValidator
// When custom security rule is required - it should be just added via DocumentValidator::addRule();
// TODO: deprecate this
if (null === self::$securityRules) {
if (self::$securityRules === null) {
self::$securityRules = [
DisableIntrospection::class => new DisableIntrospection(DisableIntrospection::DISABLED), // DEFAULT DISABLED
QueryDepth::class => new QueryDepth(QueryDepth::DISABLED), // default disabled
QueryComplexity::class => new QueryComplexity(QueryComplexity::DISABLED), // default disabled
];
}
return self::$securityRules;
}
/**
* This uses a specialized visitor which runs multiple visitors in parallel,
* while maintaining the visitor skip and break API.
*
* @param ValidationRule[] $rules
* @return Error[]
*/
public static function visitUsingRules(Schema $schema, TypeInfo $typeInfo, DocumentNode $documentNode, array $rules)
{
$context = new ValidationContext($schema, $documentNode, $typeInfo);
$visitors = [];
foreach ($rules as $rule) {
$visitors[] = $rule->getVisitor($context);
}
Visitor::visit($documentNode, Visitor::visitWithTypeInfo($typeInfo, Visitor::visitInParallel($visitors)));
return $context->getErrors();
}
/**
* Returns global validation rule by name. Standard rules are named by class name, so
* example usage for such rules:
@ -177,7 +205,7 @@ class DocumentValidator
*
* @api
* @param string $name
* @return AbstractValidationRule
* @return ValidationRule
*/
public static function getRule($name)
{
@ -187,17 +215,17 @@ class DocumentValidator
return $rules[$name];
}
$name = "GraphQL\\Validator\\Rules\\$name";
return isset($rules[$name]) ? $rules[$name] : null ;
$name = sprintf('GraphQL\\Validator\\Rules\\%s', $name);
return $rules[$name] ?? null;
}
/**
* Add rule to list of global validation rules
*
* @api
* @param AbstractValidationRule $rule
*/
public static function addRule(AbstractValidationRule $rule)
public static function addRule(ValidationRule $rule)
{
self::$rules[$rule->getName()] = $rule;
}
@ -205,7 +233,12 @@ class DocumentValidator
public static function isError($value)
{
return is_array($value)
? count(array_filter($value, function($item) { return $item instanceof \Exception || $item instanceof \Throwable;})) === count($value)
? count(array_filter(
$value,
function ($item) {
return $item instanceof \Exception || $item instanceof \Throwable;
}
)) === count($value)
: ($value instanceof \Exception || $value instanceof \Throwable);
}
@ -216,6 +249,7 @@ class DocumentValidator
} else {
$arr[] = $items;
}
return $arr;
}
@ -236,27 +270,7 @@ class DocumentValidator
$validator = new ValuesOfCorrectType();
$visitor = $validator->getVisitor($context);
Visitor::visit($valueNode, Visitor::visitWithTypeInfo($typeInfo, $visitor));
return $context->getErrors();
}
/**
* This uses a specialized visitor which runs multiple visitors in parallel,
* while maintaining the visitor skip and break API.
*
* @param Schema $schema
* @param TypeInfo $typeInfo
* @param DocumentNode $documentNode
* @param AbstractValidationRule[] $rules
* @return array
*/
public static function visitUsingRules(Schema $schema, TypeInfo $typeInfo, DocumentNode $documentNode, array $rules)
{
$context = new ValidationContext($schema, $documentNode, $typeInfo);
$visitors = [];
foreach ($rules as $rule) {
$visitors[] = $rule->getVisitor($context);
}
Visitor::visit($documentNode, Visitor::visitWithTypeInfo($typeInfo, Visitor::visitInParallel($visitors)));
return $context->getErrors();
}
}

View File

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

View File

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
@ -6,9 +9,11 @@ use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Validator\ValidationContext;
class DisableIntrospection extends AbstractQuerySecurity
class DisableIntrospection extends QuerySecurityRule
{
const ENABLED = 1;
public const ENABLED = 1;
/** @var bool */
private $isEnabled;
public function __construct($enabled = self::ENABLED)
@ -21,7 +26,26 @@ class DisableIntrospection extends AbstractQuerySecurity
$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';
}
@ -30,21 +54,4 @@ class DisableIntrospection extends AbstractQuerySecurity
{
return $this->isEnabled !== static::DISABLED;
}
public function getVisitor(ValidationContext $context)
{
return $this->invokeIfNeeded(
$context,
[
NodeKind::FIELD => function (FieldNode $node) use ($context) {
if ($node->name->value === '__type' || $node->name->value === '__schema') {
$context->reportError(new Error(
static::introspectionDisabledMessage(),
[$node]
));
}
}
]
);
}
}

View File

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

View File

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

View File

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
@ -9,43 +12,53 @@ use GraphQL\Language\Printer;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils\TypeInfo;
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)
{
return [
NodeKind::INLINE_FRAGMENT => function(InlineFragmentNode $node) use ($context) {
if ($node->typeCondition) {
NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) use ($context) {
if (! $node->typeCondition) {
return;
}
$type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition);
if ($type && !Type::isCompositeType($type)) {
if (! $type || Type::isCompositeType($type)) {
return;
}
$context->reportError(new Error(
static::inlineFragmentOnNonCompositeErrorMessage($type),
[$node->typeCondition]
));
}
}
},
NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) use ($context) {
NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) {
$type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition);
if ($type && !Type::isCompositeType($type)) {
if (! $type || Type::isCompositeType($type)) {
return;
}
$context->reportError(new Error(
static::fragmentOnNonCompositeErrorMessage($node->name->value, Printer::doPrint($node->typeCondition)),
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
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
@ -6,6 +9,9 @@ use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext;
use function array_map;
use function count;
use function sprintf;
/**
* Known argument names
@ -13,32 +19,17 @@ use GraphQL\Validator\ValidationContext;
* A GraphQL field is only valid if all supplied arguments are defined by
* 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)
{
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();
if (!$argDef) {
if ($argDef !== null) {
return;
}
$argumentOf = $ancestors[count($ancestors) - 1];
if ($argumentOf->kind === NodeKind::FIELD) {
$fieldDef = $context->getFieldDef();
@ -51,13 +42,18 @@ class KnownArgumentNames extends AbstractValidationRule
$parentType->name,
Utils::suggestionList(
$node->name->value,
array_map(function ($arg) { return $arg->name; }, $fieldDef->args)
array_map(
function ($arg) {
return $arg->name;
},
$fieldDef->args
)
)
),
[$node]
));
}
} else if ($argumentOf->kind === NodeKind::DIRECTIVE) {
} elseif ($argumentOf->kind === NodeKind::DIRECTIVE) {
$directive = $context->getDirective();
if ($directive) {
$context->reportError(new Error(
@ -66,15 +62,45 @@ class KnownArgumentNames extends AbstractValidationRule
$directive->name,
Utils::suggestionList(
$node->name->value,
array_map(function ($arg) { return $arg->name; }, $directive->args)
array_map(
function ($arg) {
return $arg->name;
},
$directive->args
)
)
),
[$node]
));
}
}
}
}
},
];
}
/**
* @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
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
@ -7,19 +10,12 @@ use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\DirectiveLocation;
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)
{
return [
@ -32,39 +28,51 @@ class KnownDirectives extends AbstractValidationRule
}
}
if (!$directiveDef) {
if (! $directiveDef) {
$context->reportError(new Error(
self::unknownDirectiveMessage($node->name->value),
[$node]
));
return;
}
$candidateLocation = $this->getDirectiveLocationForASTPath($ancestors);
if (!$candidateLocation) {
if (! $candidateLocation) {
$context->reportError(new Error(
self::misplacedDirectiveMessage($node->name->value, $node->type),
[$node]
));
} else if (!in_array($candidateLocation, $directiveDef->locations)) {
} elseif (! in_array($candidateLocation, $directiveDef->locations)) {
$context->reportError(new Error(
self::misplacedDirectiveMessage($node->name->value, $candidateLocation),
[ $node ]
[$node]
));
}
}
},
];
}
public static function unknownDirectiveMessage($directiveName)
{
return sprintf('Unknown directive "%s".', $directiveName);
}
/**
* @param (Node|NodeList)[] $ancestors
*/
private function getDirectiveLocationForASTPath(array $ancestors)
{
$appliedTo = $ancestors[count($ancestors) - 1];
switch ($appliedTo->kind) {
case NodeKind::OPERATION_DEFINITION:
switch ($appliedTo->operation) {
case 'query': return DirectiveLocation::QUERY;
case 'mutation': return DirectiveLocation::MUTATION;
case 'subscription': return DirectiveLocation::SUBSCRIPTION;
case 'query':
return DirectiveLocation::QUERY;
case 'mutation':
return DirectiveLocation::MUTATION;
case 'subscription':
return DirectiveLocation::SUBSCRIPTION;
}
break;
case NodeKind::FIELD:
@ -101,9 +109,15 @@ class KnownDirectives extends AbstractValidationRule
return DirectiveLocation::INPUT_OBJECT;
case NodeKind::INPUT_VALUE_DEFINITION:
$parentNode = $ancestors[count($ancestors) - 3];
return $parentNode instanceof InputObjectTypeDefinitionNode
? DirectiveLocation::INPUT_FIELD_DEFINITION
: DirectiveLocation::ARGUMENT_DEFINITION;
}
}
public static function misplacedDirectiveMessage($directiveName, $location)
{
return sprintf('Directive "%s" may not be used on "%s".', $directiveName, $location);
}
}

View File

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

View File

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
@ -7,6 +10,8 @@ use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Visitor;
use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext;
use function array_keys;
use function sprintf;
/**
* Known type names
@ -14,21 +19,13 @@ use GraphQL\Validator\ValidationContext;
* A GraphQL document is only valid if referenced types (specifically
* 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)
{
$skip = function() { return Visitor::skipNode(); };
$skip = function () {
return Visitor::skipNode();
};
return [
// TODO: when validating IDL, re-enable these. Experimental version does not
@ -38,19 +35,38 @@ class KnownTypeNames extends AbstractValidationRule
NodeKind::INTERFACE_TYPE_DEFINITION => $skip,
NodeKind::UNION_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();
$typeName = $node->name->value;
$type = $schema->getType($typeName);
if (!$type) {
if ($type !== null) {
return;
}
$context->reportError(new Error(
self::unknownTypeMessage(
$typeName,
Utils::suggestionList($typeName, array_keys($schema->getTypeMap()))
), [$node])
);
}
}
),
[$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
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
@ -7,6 +10,7 @@ use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext;
use function count;
/**
* Lone anonymous operation
@ -14,33 +18,40 @@ use GraphQL\Validator\ValidationContext;
* 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.
*/
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)
{
$operationCount = 0;
return [
NodeKind::DOCUMENT => function(DocumentNode $node) use (&$operationCount) {
NodeKind::DOCUMENT => function (DocumentNode $node) use (&$operationCount) {
$tmp = Utils::filter(
$node->definitions,
function ($definition) {
return $definition->kind === NodeKind::OPERATION_DEFINITION;
}
);
$operationCount = count($tmp);
},
NodeKind::OPERATION_DEFINITION => function(OperationDefinitionNode $node) use (&$operationCount, $context) {
if (!$node->name && $operationCount > 1) {
NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) use (
&$operationCount,
$context
) {
if ($node->name || $operationCount <= 1) {
return;
}
$context->reportError(
new Error(self::anonOperationNotAloneMessage(), [$node])
);
}
}
},
];
}
public static function anonOperationNotAloneMessage()
{
return 'This anonymous operation must be the only defined operation.';
}
}

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
@ -8,27 +11,18 @@ use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Visitor;
use GraphQL\Type\Definition\NonNull;
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)
{
return [
NodeKind::FIELD => [
'leave' => function(FieldNode $fieldNode) use ($context) {
'leave' => function (FieldNode $fieldNode) use ($context) {
$fieldDef = $context->getFieldDef();
if (!$fieldDef) {
if (! $fieldDef) {
return Visitor::skipNode();
}
$argNodes = $fieldNode->arguments ?: [];
@ -38,20 +32,22 @@ class ProvidedNonNullArguments extends AbstractValidationRule
$argNodeMap[$argNode->name->value] = $argNodes;
}
foreach ($fieldDef->args as $argDef) {
$argNode = isset($argNodeMap[$argDef->name]) ? $argNodeMap[$argDef->name] : null;
if (!$argNode && $argDef->getType() instanceof NonNull) {
$argNode = $argNodeMap[$argDef->name] ?? null;
if ($argNode || ! ($argDef->getType() instanceof NonNull)) {
continue;
}
$context->reportError(new Error(
self::missingFieldArgMessage($fieldNode->name->value, $argDef->name, $argDef->getType()),
[$fieldNode]
));
}
}
}
},
],
NodeKind::DIRECTIVE => [
'leave' => function(DirectiveNode $directiveNode) use ($context) {
'leave' => function (DirectiveNode $directiveNode) use ($context) {
$directiveDef = $context->getDirective();
if (!$directiveDef) {
if (! $directiveDef) {
return Visitor::skipNode();
}
$argNodes = $directiveNode->arguments ?: [];
@ -61,16 +57,42 @@ class ProvidedNonNullArguments extends AbstractValidationRule
}
foreach ($directiveDef->args as $argDef) {
$argNode = isset($argNodeMap[$argDef->name]) ? $argNodeMap[$argDef->name] : null;
if (!$argNode && $argDef->getType() instanceof NonNull) {
$argNode = $argNodeMap[$argDef->name] ?? null;
if ($argNode || ! ($argDef->getType() instanceof NonNull)) {
continue;
}
$context->reportError(new Error(
self::missingDirectiveArgMessage($directiveNode->name->value, $argDef->name, $argDef->getType()),
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
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
@ -14,20 +17,27 @@ use GraphQL\Language\Visitor;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\FieldDefinition;
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;
/** @var mixed[]|null */
private $rawVariableValues = [];
/** @var \ArrayObject */
private $variableDefs;
/** @var \ArrayObject */
private $fieldNodeAndDefs;
/**
* @var ValidationContext
*/
/** @var ValidationContext */
private $context;
public function __construct($maxQueryComplexity)
@ -35,38 +45,6 @@ class QueryComplexity extends AbstractQuerySecurity
$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)
{
$this->context = $context;
@ -89,21 +67,29 @@ class QueryComplexity extends AbstractQuerySecurity
},
NodeKind::VARIABLE_DEFINITION => function ($def) {
$this->variableDefs[] = $def;
return Visitor::skipNode();
},
NodeKind::OPERATION_DEFINITION => [
'leave' => function (OperationDefinitionNode $operationDefinition) use ($context, &$complexity) {
$errors = $context->getErrors();
if (empty($errors)) {
if (! empty($errors)) {
return;
}
$complexity = $this->fieldComplexity($operationDefinition, $complexity);
if ($complexity > $this->getMaxQueryComplexity()) {
if ($complexity <= $this->getMaxQueryComplexity()) {
return;
}
$context->reportError(
new Error($this->maxQueryComplexityErrorMessage($this->getMaxQueryComplexity(), $complexity))
new Error($this->maxQueryComplexityErrorMessage(
$this->getMaxQueryComplexity(),
$complexity
))
);
}
}
},
],
]
@ -125,7 +111,7 @@ class QueryComplexity extends AbstractQuerySecurity
{
switch ($node->kind) {
case NodeKind::FIELD:
/* @var FieldNode $node */
/** @var FieldNode $node */
// default values
$args = [];
$complexityFn = FieldDefinition::DEFAULT_COMPLEXITY_FN;
@ -157,7 +143,7 @@ class QueryComplexity extends AbstractQuerySecurity
break;
case NodeKind::INLINE_FRAGMENT:
/* @var InlineFragmentNode $node */
/** @var InlineFragmentNode $node */
// node has children?
if (isset($node->selectionSet)) {
$complexity = $this->fieldComplexity($node, $complexity);
@ -165,10 +151,10 @@ class QueryComplexity extends AbstractQuerySecurity
break;
case NodeKind::FRAGMENT_SPREAD:
/* @var FragmentSpreadNode $node */
/** @var FragmentSpreadNode $node */
$fragment = $this->getFragment($node);
if (null !== $fragment) {
if ($fragment !== null) {
$complexity = $this->fieldComplexity($fragment, $complexity);
}
break;
@ -183,7 +169,7 @@ class QueryComplexity extends AbstractQuerySecurity
$astFieldInfo = [null, null];
if (isset($this->fieldNodeAndDefs[$fieldName])) {
foreach ($this->fieldNodeAndDefs[$fieldName] as $astAndDef) {
if ($astAndDef[0] == $field) {
if ($astAndDef[0] === $field) {
$astFieldInfo = $astAndDef;
break;
}
@ -193,6 +179,59 @@ class QueryComplexity extends AbstractQuerySecurity
return $astFieldInfo;
}
private function directiveExcludesField(FieldNode $node)
{
foreach ($node->directives as $directiveNode) {
if ($directiveNode->name->value === 'deprecated') {
return false;
}
$variableValuesResult = Values::getVariableValues(
$this->context->getSchema(),
$this->variableDefs,
$this->getRawVariableValues()
);
if ($variableValuesResult['errors']) {
throw new Error(implode(
"\n\n",
array_map(
function ($error) {
return $error->getMessage();
},
$variableValuesResult['errors']
)
));
}
$variableValues = $variableValuesResult['coerced'];
if ($directiveNode->name->value === 'include') {
$directive = Directive::includeDirective();
$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();
@ -209,11 +248,15 @@ class QueryComplexity extends AbstractQuerySecurity
);
if ($variableValuesResult['errors']) {
throw new Error(implode("\n\n", array_map(
throw new Error(implode(
"\n\n",
array_map(
function ($error) {
return $error->getMessage();
}
, $variableValuesResult['errors'])));
},
$variableValuesResult['errors']
)
));
}
$variableValues = $variableValuesResult['coerced'];
@ -223,39 +266,24 @@ class QueryComplexity extends AbstractQuerySecurity
return $args;
}
private function directiveExcludesField(FieldNode $node) {
foreach ($node->directives as $directiveNode) {
if ($directiveNode->name->value === 'deprecated') {
return false;
public function getMaxQueryComplexity()
{
return $this->maxQueryComplexity;
}
$variableValuesResult = Values::getVariableValues(
$this->context->getSchema(),
$this->variableDefs,
$this->getRawVariableValues()
);
/**
* 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);
if ($variableValuesResult['errors']) {
throw new Error(implode("\n\n", array_map(
function ($error) {
return $error->getMessage();
$this->maxQueryComplexity = (int) $maxQueryComplexity;
}
, $variableValuesResult['errors'])));
}
$variableValues = $variableValuesResult['coerced'];
if ($directiveNode->name->value === 'include') {
$directive = Directive::includeDirective();
$directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues);
return !$directiveArgs['if'];
} else {
$directive = Directive::skipDirective();
$directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues);
return $directiveArgs['if'];
}
}
public static function maxQueryComplexityErrorMessage($max, $count)
{
return sprintf('Max query complexity should be %d but got %d.', $max, $count);
}
protected function isEnabled()

View File

@ -1,21 +1,20 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
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\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Validator\ValidationContext;
use function sprintf;
class QueryDepth extends AbstractQuerySecurity
class QueryDepth extends QuerySecurityRule
{
/**
* @var int
*/
/** @var int */
private $maxQueryDepth;
public function __construct($maxQueryDepth)
@ -23,28 +22,6 @@ class QueryDepth extends AbstractQuerySecurity
$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)
{
return $this->invokeIfNeeded(
@ -54,22 +31,19 @@ class QueryDepth extends AbstractQuerySecurity
'leave' => function (OperationDefinitionNode $operationDefinition) use ($context) {
$maxDepth = $this->fieldDepth($operationDefinition);
if ($maxDepth > $this->getMaxQueryDepth()) {
if ($maxDepth <= $this->getMaxQueryDepth()) {
return;
}
$context->reportError(
new Error($this->maxQueryDepthErrorMessage($this->getMaxQueryDepth(), $maxDepth))
);
}
},
],
]
);
}
protected function isEnabled()
{
return $this->getMaxQueryDepth() !== static::DISABLED;
}
private function fieldDepth($node, $depth = 0, $maxDepth = 0)
{
if (isset($node->selectionSet) && $node->selectionSet instanceof SelectionSetNode) {
@ -85,9 +59,9 @@ class QueryDepth extends AbstractQuerySecurity
{
switch ($node->kind) {
case NodeKind::FIELD:
/* @var FieldNode $node */
/** @var FieldNode $node */
// node has children?
if (null !== $node->selectionSet) {
if ($node->selectionSet !== null) {
// update maxDepth if needed
if ($depth > $maxDepth) {
$maxDepth = $depth;
@ -97,18 +71,18 @@ class QueryDepth extends AbstractQuerySecurity
break;
case NodeKind::INLINE_FRAGMENT:
/* @var InlineFragmentNode $node */
/** @var InlineFragmentNode $node */
// node has children?
if (null !== $node->selectionSet) {
if ($node->selectionSet !== null) {
$maxDepth = $this->fieldDepth($node, $depth, $maxDepth);
}
break;
case NodeKind::FRAGMENT_SPREAD:
/* @var FragmentSpreadNode $node */
/** @var FragmentSpreadNode $node */
$fragment = $this->getFragment($node);
if (null !== $fragment) {
if ($fragment !== null) {
$maxDepth = $this->fieldDepth($fragment, $depth, $maxDepth);
}
break;
@ -116,4 +90,31 @@ class QueryDepth extends AbstractQuerySecurity
return $maxDepth;
}
public function getMaxQueryDepth()
{
return $this->maxQueryDepth;
}
/**
* Set max query depth. If equal to 0 no check is done. Must be greater or equal to 0.
*
* @param int $maxQueryDepth
*/
public function setMaxQueryDepth($maxQueryDepth)
{
$this->checkIfGreaterOrEqualToZero('maxQueryDepth', $maxQueryDepth);
$this->maxQueryDepth = (int) $maxQueryDepth;
}
public static function maxQueryDepthErrorMessage($max, $count)
{
return sprintf('Max query depth should be %d but got %d.', $max, $count);
}
protected function isEnabled()
{
return $this->getMaxQueryDepth() !== static::DISABLED;
}
}

View File

@ -1,40 +1,35 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use Closure;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Introspection;
use GraphQL\Utils\TypeInfo;
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 = [];
/**
* @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.
*
* @param $value
* @param string $name
* @param int $value
*/
protected function checkIfGreaterOrEqualToZero($name, $value)
{
@ -43,30 +38,30 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule
}
}
protected function gatherFragmentDefinition(ValidationContext $context)
{
// Gather all the fragment definition.
// Importantly this does not include inline fragments.
$definitions = $context->getDocument()->definitions;
foreach ($definitions as $node) {
if ($node instanceof FragmentDefinitionNode) {
$this->fragments[$node->name->value] = $node;
}
}
}
protected function getFragment(FragmentSpreadNode $fragmentSpread)
{
$spreadName = $fragmentSpread->name->value;
$fragments = $this->getFragments();
return isset($fragments[$spreadName]) ? $fragments[$spreadName] : null;
return $fragments[$spreadName] ?? null;
}
/**
* @return FragmentDefinitionNode[]
*/
protected function getFragments()
{
return $this->fragments;
}
/**
* @param Closure[] $validators
* @return Closure[]
*/
protected function invokeIfNeeded(ValidationContext $context, array $validators)
{
// is disabled?
if (!$this->isEnabled()) {
if (! $this->isEnabled()) {
return [];
}
@ -75,6 +70,22 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule
return $validators;
}
abstract protected function isEnabled();
protected function gatherFragmentDefinition(ValidationContext $context)
{
// Gather all the fragment definition.
// Importantly this does not include inline fragments.
$definitions = $context->getDocument()->definitions;
foreach ($definitions as $node) {
if (! ($node instanceof FragmentDefinitionNode)) {
continue;
}
$this->fragments[$node->name->value] = $node;
}
}
/**
* Given a selectionSet, adds all of the fields in that selection to
* the passed in map of fields, and returns it at the end.
@ -85,23 +96,24 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule
*
* @see \GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged
*
* @param ValidationContext $context
* @param Type|null $parentType
* @param SelectionSetNode $selectionSet
* @param \ArrayObject $visitedFragmentNames
* @param \ArrayObject $astAndDefs
*
* @return \ArrayObject
*/
protected function collectFieldASTsAndDefs(ValidationContext $context, $parentType, SelectionSetNode $selectionSet, \ArrayObject $visitedFragmentNames = null, \ArrayObject $astAndDefs = null)
{
protected function collectFieldASTsAndDefs(
ValidationContext $context,
$parentType,
SelectionSetNode $selectionSet,
?\ArrayObject $visitedFragmentNames = null,
?\ArrayObject $astAndDefs = null
) {
$_visitedFragmentNames = $visitedFragmentNames ?: new \ArrayObject();
$_astAndDefs = $astAndDefs ?: new \ArrayObject();
foreach ($selectionSet->selections as $selection) {
switch ($selection->kind) {
switch ($selection->getKind()) {
case NodeKind::FIELD:
/* @var FieldNode $selection */
/** @var FieldNode $selection */
$fieldName = $selection->name->value;
$fieldDef = null;
if ($parentType && method_exists($parentType, 'getFields')) {
@ -121,14 +133,14 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule
}
}
$responseName = $this->getFieldName($selection);
if (!isset($_astAndDefs[$responseName])) {
if (! isset($_astAndDefs[$responseName])) {
$_astAndDefs[$responseName] = new \ArrayObject();
}
// create field context
$_astAndDefs[$responseName][] = [$selection, $fieldDef];
break;
case NodeKind::INLINE_FRAGMENT:
/* @var InlineFragmentNode $selection */
/** @var InlineFragmentNode $selection */
$_astAndDefs = $this->collectFieldASTsAndDefs(
$context,
TypeInfo::typeFromAST($context->getSchema(), $selection->typeCondition),
@ -138,7 +150,7 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule
);
break;
case NodeKind::FRAGMENT_SPREAD:
/* @var FragmentSpreadNode $selection */
/** @var FragmentSpreadNode $selection */
$fragName = $selection->name->value;
if (empty($_visitedFragmentNames[$fragName])) {
@ -165,10 +177,9 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule
protected function getFieldName(FieldNode $node)
{
$fieldName = $node->name->value;
$responseName = $node->alias ? $node->alias->value : $fieldName;
return $responseName;
return $node->alias ? $node->alias->value : $fieldName;
}
abstract protected function isEnabled();
}
class_alias(QuerySecurityRule::class, 'GraphQL\Validator\Rules\AbstractQuerySecurity');

View File

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
@ -6,25 +9,19 @@ use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Type\Definition\Type;
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)
{
return [
NodeKind::FIELD => function(FieldNode $node) use ($context) {
NodeKind::FIELD => function (FieldNode $node) use ($context) {
$type = $context->getType();
if ($type) {
if (! $type) {
return;
}
if (Type::isLeafType(Type::getNamedType($type))) {
if ($node->selectionSet) {
$context->reportError(new Error(
@ -32,14 +29,23 @@ class ScalarLeafs extends AbstractValidationRule
[$node->selectionSet]
));
}
} else if (!$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
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NameNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Visitor;
use GraphQL\Validator\ValidationContext;
use function sprintf;
class UniqueArgumentNames extends AbstractValidationRule
class UniqueArgumentNames extends ValidationRule
{
static function duplicateArgMessage($argName)
{
return "There can be only one argument named \"$argName\".";
}
/** @var NameNode[] */
public $knownArgNames;
public function getVisitor(ValidationContext $context)
@ -23,14 +23,14 @@ class UniqueArgumentNames extends AbstractValidationRule
return [
NodeKind::FIELD => function () {
$this->knownArgNames = [];;
$this->knownArgNames = [];
},
NodeKind::DIRECTIVE => function () {
$this->knownArgNames = [];
},
NodeKind::ARGUMENT => function (ArgumentNode $node) use ($context) {
$argName = $node->name->value;
if (!empty($this->knownArgNames[$argName])) {
if (! empty($this->knownArgNames[$argName])) {
$context->reportError(new Error(
self::duplicateArgMessage($argName),
[$this->knownArgNames[$argName], $node->name]
@ -38,8 +38,14 @@ class UniqueArgumentNames extends AbstractValidationRule
} else {
$this->knownArgNames[$argName] = $node->name;
}
return Visitor::skipNode();
}
},
];
}
public static function duplicateArgMessage($argName)
{
return sprintf('There can be only one argument named "%s".', $argName);
}
}

View File

@ -1,23 +1,25 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\Node;
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)
{
return [
'enter' => function(Node $node) use ($context) {
if (isset($node->directives)) {
'enter' => function (Node $node) use ($context) {
if (! isset($node->directives)) {
return;
}
$knownDirectives = [];
foreach ($node->directives as $directive) {
/** @var DirectiveNode $directive */
@ -31,8 +33,12 @@ class UniqueDirectivesPerLocation extends AbstractValidationRule
$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
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\NameNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Visitor;
use GraphQL\Validator\ValidationContext;
use function sprintf;
class UniqueFragmentNames extends AbstractValidationRule
class UniqueFragmentNames extends ValidationRule
{
static function duplicateFragmentNameMessage($fragName)
{
return "There can be only one fragment named \"$fragName\".";
}
/** @var NameNode[] */
public $knownFragmentNames;
public function getVisitor(ValidationContext $context)
@ -26,16 +27,22 @@ class UniqueFragmentNames extends AbstractValidationRule
},
NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) {
$fragmentName = $node->name->value;
if (!empty($this->knownFragmentNames[$fragmentName])) {
if (empty($this->knownFragmentNames[$fragmentName])) {
$this->knownFragmentNames[$fragmentName] = $node->name;
} else {
$context->reportError(new Error(
self::duplicateFragmentNameMessage($fragmentName),
[ $this->knownFragmentNames[$fragmentName], $node->name ]
[$this->knownFragmentNames[$fragmentName], $node->name]
));
} else {
$this->knownFragmentNames[$fragmentName] = $node->name;
}
return Visitor::skipNode();
}
},
];
}
public static function duplicateFragmentNameMessage($fragName)
{
return sprintf('There can be only one fragment named "%s".', $fragName);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,39 +1,42 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Language\Printer;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils\TypeInfo;
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)
{
return [
NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $node) use ($context) {
NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) use ($context) {
$type = TypeInfo::typeFromAST($context->getSchema(), $node->type);
// If the variable type is not an input type, return an error.
if ($type && !Type::isInputType($type)) {
if (! $type || Type::isInputType($type)) {
return;
}
$variableName = $node->variable->name->value;
$context->reportError(new Error(
self::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)),
[ $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
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
@ -9,6 +12,7 @@ use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Language\Visitor;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Validator\ValidationContext;
use function sprintf;
/**
* Variable's default value is allowed
@ -16,21 +20,12 @@ use GraphQL\Validator\ValidationContext;
* A GraphQL document is only valid if all variable default values are allowed
* 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)
{
return [
NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $node) use ($context) {
NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) use ($context) {
$name = $node->variable->name->value;
$defaultValue = $node->defaultValue;
$type = $context->getInputType();
@ -49,12 +44,22 @@ class VariablesDefaultValueAllowed extends AbstractValidationRule
return Visitor::skipNode();
},
NodeKind::SELECTION_SET => function(SelectionSetNode $node) use ($context) {
NodeKind::SELECTION_SET => function (SelectionSetNode $node) use ($context) {
return Visitor::skipNode();
},
NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) use ($context) {
NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) {
return Visitor::skipNode();
},
];
}
public static function defaultForRequiredVarMessage($varName, $type, $guessType)
{
return sprintf(
'Variable "$%s" of type "%s" is required and will not use the default value. Perhaps you meant to use type "%s".',
$varName,
$type,
$guessType
);
}
}

View File

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

View File

@ -1,22 +1,27 @@
<?php
declare(strict_types=1);
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\HasSelectionSet;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\VariableNode;
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\InputType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
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,
@ -25,59 +30,34 @@ use GraphQL\Utils\TypeInfo;
*/
class ValidationContext
{
/**
* @var Schema
*/
/** @var Schema */
private $schema;
/**
* @var DocumentNode
*/
/** @var DocumentNode */
private $ast;
/**
* @var TypeInfo
*/
/** @var TypeInfo */
private $typeInfo;
/**
* @var Error[]
*/
/** @var Error[] */
private $errors;
/**
* @var FragmentDefinitionNode[]
*/
/** @var FragmentDefinitionNode[] */
private $fragments;
/**
* @var SplObjectStorage
*/
/** @var SplObjectStorage */
private $fragmentSpreads;
/**
* @var SplObjectStorage
*/
/** @var SplObjectStorage */
private $recursivelyReferencedFragments;
/**
* @var SplObjectStorage
*/
/** @var SplObjectStorage */
private $variableUsages;
/**
* @var SplObjectStorage
*/
/** @var SplObjectStorage */
private $recursiveVariableUsages;
/**
* ValidationContext constructor.
*
* @param Schema $schema
* @param DocumentNode $ast
* @param TypeInfo $typeInfo
*/
function __construct(Schema $schema, DocumentNode $ast, TypeInfo $typeInfo)
public function __construct(Schema $schema, DocumentNode $ast, TypeInfo $typeInfo)
{
$this->schema = $schema;
$this->ast = $ast;
@ -89,10 +69,7 @@ class ValidationContext
$this->recursiveVariableUsages = new SplObjectStorage();
}
/**
* @param Error $error
*/
function reportError(Error $error)
public function reportError(Error $error)
{
$this->errors[] = $error;
}
@ -100,7 +77,7 @@ class ValidationContext
/**
* @return Error[]
*/
function getErrors()
public function getErrors()
{
return $this->errors;
}
@ -108,132 +85,19 @@ class ValidationContext
/**
* @return Schema
*/
function getSchema()
public function getSchema()
{
return $this->schema;
}
/**
* @return DocumentNode
* @return mixed[][] List of ['node' => VariableNode, 'type' => ?InputObjectType]
*/
function getDocument()
public function getRecursiveVariableUsages(OperationDefinitionNode $operation)
{
return $this->ast;
}
$usages = $this->recursiveVariableUsages[$operation] ?? null;
/**
* @param string $name
* @return FragmentDefinitionNode|null
*/
function getFragment($name)
{
$fragments = $this->fragments;
if (!$fragments) {
$fragments = [];
foreach ($this->getDocument()->definitions as $statement) {
if ($statement->kind === NodeKind::FRAGMENT_DEFINITION) {
$fragments[$statement->name->value] = $statement;
}
}
$this->fragments = $fragments;
}
return isset($fragments[$name]) ? $fragments[$name] : null;
}
/**
* @param HasSelectionSet $node
* @return FragmentSpreadNode[]
*/
function getFragmentSpreads(HasSelectionSet $node)
{
$spreads = isset($this->fragmentSpreads[$node]) ? $this->fragmentSpreads[$node] : null;
if (!$spreads) {
$spreads = [];
$setsToVisit = [$node->selectionSet];
while (!empty($setsToVisit)) {
$set = array_pop($setsToVisit);
for ($i = 0; $i < count($set->selections); $i++) {
$selection = $set->selections[$i];
if ($selection->kind === NodeKind::FRAGMENT_SPREAD) {
$spreads[] = $selection;
} else if ($selection->selectionSet) {
$setsToVisit[] = $selection->selectionSet;
}
}
}
$this->fragmentSpreads[$node] = $spreads;
}
return $spreads;
}
/**
* @param OperationDefinitionNode $operation
* @return FragmentDefinitionNode[]
*/
function getRecursivelyReferencedFragments(OperationDefinitionNode $operation)
{
$fragments = isset($this->recursivelyReferencedFragments[$operation]) ? $this->recursivelyReferencedFragments[$operation] : null;
if (!$fragments) {
$fragments = [];
$collectedNames = [];
$nodesToVisit = [$operation];
while (!empty($nodesToVisit)) {
$node = array_pop($nodesToVisit);
$spreads = $this->getFragmentSpreads($node);
for ($i = 0; $i < count($spreads); $i++) {
$fragName = $spreads[$i]->name->value;
if (empty($collectedNames[$fragName])) {
$collectedNames[$fragName] = true;
$fragment = $this->getFragment($fragName);
if ($fragment) {
$fragments[] = $fragment;
$nodesToVisit[] = $fragment;
}
}
}
}
$this->recursivelyReferencedFragments[$operation] = $fragments;
}
return $fragments;
}
/**
* @param HasSelectionSet $node
* @return array List of ['node' => VariableNode, 'type' => ?InputObjectType]
*/
function getVariableUsages(HasSelectionSet $node)
{
$usages = isset($this->variableUsages[$node]) ? $this->variableUsages[$node] : null;
if (!$usages) {
$newUsages = [];
$typeInfo = new TypeInfo($this->schema);
Visitor::visit($node, Visitor::visitWithTypeInfo($typeInfo, [
NodeKind::VARIABLE_DEFINITION => function () {
return false;
},
NodeKind::VARIABLE => function (VariableNode $variable) use (&$newUsages, $typeInfo) {
$newUsages[] = ['node' => $variable, 'type' => $typeInfo->getInputType()];
}
]));
$usages = $newUsages;
$this->variableUsages[$node] = $usages;
}
return $usages;
}
/**
* @param OperationDefinitionNode $operation
* @return array List of ['node' => VariableNode, 'type' => ?InputObjectType]
*/
function getRecursiveVariableUsages(OperationDefinitionNode $operation)
{
$usages = isset($this->recursiveVariableUsages[$operation]) ? $this->recursiveVariableUsages[$operation] : null;
if (!$usages) {
if (! $usages) {
$usages = $this->getVariableUsages($operation);
$fragments = $this->getRecursivelyReferencedFragments($operation);
@ -244,23 +108,152 @@ class ValidationContext
$usages = call_user_func_array('array_merge', $tmp);
$this->recursiveVariableUsages[$operation] = $usages;
}
return $usages;
}
/**
* @return mixed[][] List of ['node' => VariableNode, 'type' => ?InputObjectType]
*/
private function getVariableUsages(HasSelectionSet $node)
{
$usages = $this->variableUsages[$node] ?? null;
if (! $usages) {
$newUsages = [];
$typeInfo = new TypeInfo($this->schema);
Visitor::visit(
$node,
Visitor::visitWithTypeInfo(
$typeInfo,
[
NodeKind::VARIABLE_DEFINITION => function () {
return false;
},
NodeKind::VARIABLE => function (VariableNode $variable) use (
&$newUsages,
$typeInfo
) {
$newUsages[] = ['node' => $variable, 'type' => $typeInfo->getInputType()];
},
]
)
);
$usages = $newUsages;
$this->variableUsages[$node] = $usages;
}
return $usages;
}
/**
* @return FragmentDefinitionNode[]
*/
public function getRecursivelyReferencedFragments(OperationDefinitionNode $operation)
{
$fragments = $this->recursivelyReferencedFragments[$operation] ?? null;
if (! $fragments) {
$fragments = [];
$collectedNames = [];
$nodesToVisit = [$operation];
while (! empty($nodesToVisit)) {
$node = array_pop($nodesToVisit);
$spreads = $this->getFragmentSpreads($node);
for ($i = 0; $i < count($spreads); $i++) {
$fragName = $spreads[$i]->name->value;
if (! empty($collectedNames[$fragName])) {
continue;
}
$collectedNames[$fragName] = true;
$fragment = $this->getFragment($fragName);
if (! $fragment) {
continue;
}
$fragments[] = $fragment;
$nodesToVisit[] = $fragment;
}
}
$this->recursivelyReferencedFragments[$operation] = $fragments;
}
return $fragments;
}
/**
* @return FragmentSpreadNode[]
*/
public function getFragmentSpreads(HasSelectionSet $node)
{
$spreads = $this->fragmentSpreads[$node] ?? null;
if (! $spreads) {
$spreads = [];
$setsToVisit = [$node->selectionSet];
while (! empty($setsToVisit)) {
$set = array_pop($setsToVisit);
for ($i = 0; $i < count($set->selections); $i++) {
$selection = $set->selections[$i];
if ($selection->kind === NodeKind::FRAGMENT_SPREAD) {
$spreads[] = $selection;
} elseif ($selection->selectionSet) {
$setsToVisit[] = $selection->selectionSet;
}
}
}
$this->fragmentSpreads[$node] = $spreads;
}
return $spreads;
}
/**
* @param string $name
* @return FragmentDefinitionNode|null
*/
public function getFragment($name)
{
$fragments = $this->fragments;
if (! $fragments) {
$fragments = [];
foreach ($this->getDocument()->definitions as $statement) {
if ($statement->kind !== NodeKind::FRAGMENT_DEFINITION) {
continue;
}
$fragments[$statement->name->value] = $statement;
}
$this->fragments = $fragments;
}
return $fragments[$name] ?? null;
}
/**
* @return DocumentNode
*/
public function getDocument()
{
return $this->ast;
}
/**
* Returns OutputType
*
* @return Type
*/
function getType()
public function getType()
{
return $this->typeInfo->getType();
}
/**
* @return CompositeType
* @return Type
*/
function getParentType()
public function getParentType()
{
return $this->typeInfo->getParentType();
}
@ -268,7 +261,7 @@ class ValidationContext
/**
* @return InputType
*/
function getInputType()
public function getInputType()
{
return $this->typeInfo->getInputType();
}
@ -276,7 +269,7 @@ class ValidationContext
/**
* @return InputType
*/
function getParentInputType()
public function getParentInputType()
{
return $this->typeInfo->getParentInputType();
}
@ -284,17 +277,17 @@ class ValidationContext
/**
* @return FieldDefinition
*/
function getFieldDef()
public function getFieldDef()
{
return $this->typeInfo->getFieldDef();
}
function getDirective()
public function getDirective()
{
return $this->typeInfo->getDirective();
}
function getArgument()
public function getArgument()
{
return $this->typeInfo->getArgument();
}

View File

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