Version 4.6.0

This commit is contained in:
Neo committed 2025-12-19 05:55:31 -08:00
1 parent f79dcf067a
commit 517a5e1f70
2036 files changed
+110041 -26162

No files matched your search

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator;
use GraphQL\Error\Error;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Type\Schema;
abstract class ASTValidationContext
{
/** @var DocumentNode */
protected $ast;
/** @var Error[] */
protected $errors;
/** @var Schema */
protected $schema;
public function __construct(DocumentNode $ast, ?Schema $schema = null)
{
$this->ast = $ast;
$this->schema = $schema;
$this->errors = [];
}
public function reportError(Error $error)
{
$this->errors[] = $error;
}
/**
* @return Error[]
*/
public function getErrors()
{
return $this->errors;
}
/**
* @return DocumentNode
*/
public function getDocument()
{
return $this->ast;
}
public function getSchema() : ?Schema
{
return $this->schema;
}
}
+192 -173
View File
@@ -1,52 +1,56 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator;
use Exception;
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\ListValueNode;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NullValueNode;
use GraphQL\Language\AST\VariableNode;
use GraphQL\Language\Printer;
use GraphQL\Language\Visitor;
use GraphQL\Type\Schema;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\LeafType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils\Utils;
use GraphQL\Type\Schema;
use GraphQL\Utils\TypeInfo;
use GraphQL\Validator\Rules\AbstractValidationRule;
use GraphQL\Validator\Rules\ArgumentsOfCorrectType;
use GraphQL\Validator\Rules\DefaultValuesOfCorrectType;
use GraphQL\Validator\Rules\DisableIntrospection;
use GraphQL\Validator\Rules\ExecutableDefinitions;
use GraphQL\Validator\Rules\FieldsOnCorrectType;
use GraphQL\Validator\Rules\FragmentsOnCompositeTypes;
use GraphQL\Validator\Rules\KnownArgumentNames;
use GraphQL\Validator\Rules\KnownArgumentNamesOnDirectives;
use GraphQL\Validator\Rules\KnownDirectives;
use GraphQL\Validator\Rules\KnownFragmentNames;
use GraphQL\Validator\Rules\KnownTypeNames;
use GraphQL\Validator\Rules\LoneAnonymousOperation;
use GraphQL\Validator\Rules\LoneSchemaDefinition;
use GraphQL\Validator\Rules\NoFragmentCycles;
use GraphQL\Validator\Rules\NoUndefinedVariables;
use GraphQL\Validator\Rules\NoUnusedFragments;
use GraphQL\Validator\Rules\NoUnusedVariables;
use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
use GraphQL\Validator\Rules\PossibleFragmentSpreads;
use GraphQL\Validator\Rules\ProvidedNonNullArguments;
use GraphQL\Validator\Rules\ProvidedRequiredArguments;
use GraphQL\Validator\Rules\ProvidedRequiredArgumentsOnDirectives;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\QueryDepth;
use GraphQL\Validator\Rules\QuerySecurityRule;
use GraphQL\Validator\Rules\ScalarLeafs;
use GraphQL\Validator\Rules\SingleFieldSubscription;
use GraphQL\Validator\Rules\UniqueArgumentNames;
use GraphQL\Validator\Rules\UniqueDirectivesPerLocation;
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\VariablesInAllowedPosition;
use Throwable;
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.
@@ -57,7 +61,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),
@@ -68,50 +72,61 @@ 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 ValidationRule[]|null */
private static $sdlRules;
/** @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[]
*
* @api
*/
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();
}
$typeInfo = $typeInfo ?: new TypeInfo($schema);
$errors = static::visitUsingRules($schema, $typeInfo, $ast, $rules);
return $errors;
}
if (is_array($rules) === true && count($rules) === 0) {
// Skip validation if there are no rules
return [];
}
$typeInfo = $typeInfo ?? new TypeInfo($schema);
return static::visitUsingRules($schema, $typeInfo, $ast, $rules);
}
/**
* Returns all global validation rules.
*
* @return ValidationRule[]
*
* @api
* @return AbstractValidationRule[]
*/
public static function allRules()
{
if (!self::$initRules) {
static::$rules = array_merge(static::defaultRules(), self::securityRules(), self::$rules);
if (! self::$initRules) {
static::$rules = array_merge(static::defaultRules(), self::securityRules(), self::$rules);
static::$initRules = true;
}
@@ -120,33 +135,34 @@ class DocumentValidator
public static function defaultRules()
{
if (null === self::$defaultRules) {
if (self::$defaultRules === null) {
self::$defaultRules = [
UniqueOperationNames::class => new UniqueOperationNames(),
LoneAnonymousOperation::class => new LoneAnonymousOperation(),
KnownTypeNames::class => new KnownTypeNames(),
FragmentsOnCompositeTypes::class => new FragmentsOnCompositeTypes(),
VariablesAreInputTypes::class => new VariablesAreInputTypes(),
ScalarLeafs::class => new ScalarLeafs(),
FieldsOnCorrectType::class => new FieldsOnCorrectType(),
UniqueFragmentNames::class => new UniqueFragmentNames(),
KnownFragmentNames::class => new KnownFragmentNames(),
NoUnusedFragments::class => new NoUnusedFragments(),
PossibleFragmentSpreads::class => new PossibleFragmentSpreads(),
NoFragmentCycles::class => new NoFragmentCycles(),
UniqueVariableNames::class => new UniqueVariableNames(),
NoUndefinedVariables::class => new NoUndefinedVariables(),
NoUnusedVariables::class => new NoUnusedVariables(),
KnownDirectives::class => new KnownDirectives(),
UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
KnownArgumentNames::class => new KnownArgumentNames(),
UniqueArgumentNames::class => new UniqueArgumentNames(),
ArgumentsOfCorrectType::class => new ArgumentsOfCorrectType(),
ProvidedNonNullArguments::class => new ProvidedNonNullArguments(),
DefaultValuesOfCorrectType::class => new DefaultValuesOfCorrectType(),
VariablesInAllowedPosition::class => new VariablesInAllowedPosition(),
ExecutableDefinitions::class => new ExecutableDefinitions(),
UniqueOperationNames::class => new UniqueOperationNames(),
LoneAnonymousOperation::class => new LoneAnonymousOperation(),
SingleFieldSubscription::class => new SingleFieldSubscription(),
KnownTypeNames::class => new KnownTypeNames(),
FragmentsOnCompositeTypes::class => new FragmentsOnCompositeTypes(),
VariablesAreInputTypes::class => new VariablesAreInputTypes(),
ScalarLeafs::class => new ScalarLeafs(),
FieldsOnCorrectType::class => new FieldsOnCorrectType(),
UniqueFragmentNames::class => new UniqueFragmentNames(),
KnownFragmentNames::class => new KnownFragmentNames(),
NoUnusedFragments::class => new NoUnusedFragments(),
PossibleFragmentSpreads::class => new PossibleFragmentSpreads(),
NoFragmentCycles::class => new NoFragmentCycles(),
UniqueVariableNames::class => new UniqueVariableNames(),
NoUndefinedVariables::class => new NoUndefinedVariables(),
NoUnusedVariables::class => new NoUnusedVariables(),
KnownDirectives::class => new KnownDirectives(),
UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
KnownArgumentNames::class => new KnownArgumentNames(),
UniqueArgumentNames::class => new UniqueArgumentNames(),
ValuesOfCorrectType::class => new ValuesOfCorrectType(),
ProvidedRequiredArguments::class => new ProvidedRequiredArguments(),
VariablesInAllowedPosition::class => new VariablesInAllowedPosition(),
OverlappingFieldsCanBeMerged::class => new OverlappingFieldsCanBeMerged(),
UniqueInputFieldNames::class => new UniqueInputFieldNames(),
UniqueInputFieldNames::class => new UniqueInputFieldNames(),
];
}
@@ -154,7 +170,7 @@ class DocumentValidator
}
/**
* @return array
* @return QuerySecurityRule[]
*/
public static function securityRules()
{
@@ -162,25 +178,65 @@ class DocumentValidator
// When custom security rule is required - it should be just added via DocumentValidator::addRule();
// TODO: deprecate this
if (null === self::$securityRules) {
if (self::$securityRules === null) {
self::$securityRules = [
DisableIntrospection::class => new DisableIntrospection(DisableIntrospection::DISABLED), // DEFAULT DISABLED
QueryDepth::class => new QueryDepth(QueryDepth::DISABLED), // default disabled
QueryComplexity::class => new QueryComplexity(QueryComplexity::DISABLED), // default disabled
QueryDepth::class => new QueryDepth(QueryDepth::DISABLED), // default disabled
QueryComplexity::class => new QueryComplexity(QueryComplexity::DISABLED), // default disabled
];
}
return self::$securityRules;
}
public static function sdlRules()
{
if (self::$sdlRules === null) {
self::$sdlRules = [
LoneSchemaDefinition::class => new LoneSchemaDefinition(),
KnownDirectives::class => new KnownDirectives(),
KnownArgumentNamesOnDirectives::class => new KnownArgumentNamesOnDirectives(),
UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
UniqueArgumentNames::class => new UniqueArgumentNames(),
UniqueInputFieldNames::class => new UniqueInputFieldNames(),
ProvidedRequiredArgumentsOnDirectives::class => new ProvidedRequiredArgumentsOnDirectives(),
];
}
return self::$sdlRules;
}
/**
* 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:
*
* $rule = DocumentValidator::getRule(GraphQL\Validator\Rules\QueryComplexity::class);
*
* @api
* @param string $name
* @return AbstractValidationRule
*
* @return ValidationRule
*
* @api
*/
public static function getRule($name)
{
@@ -190,17 +246,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;
}
@@ -208,8 +264,13 @@ 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)
: ($value instanceof \Exception || $value instanceof \Throwable);
? count(array_filter(
$value,
static function ($item) : bool {
return $item instanceof Throwable;
}
)) === count($value)
: $value instanceof Throwable;
}
public static function append(&$arr, $items)
@@ -219,124 +280,82 @@ class DocumentValidator
} else {
$arr[] = $items;
}
return $arr;
}
/**
* Utility for validators which determines if a value literal AST is valid given
* an input type.
* Utility which determines if a value literal node is valid for an input type.
*
* Note that this only validates literal values, variables are assumed to
* provide values of the correct type.
* Deprecated. Rely on validation for documents co
* ntaining literal values.
*
* @return array
* @deprecated
*
* @return Error[]
*/
public static function isValidLiteralValue(Type $type, $valueNode)
{
// A value must be provided if the type is non-null.
if ($type instanceof NonNull) {
if (!$valueNode || $valueNode instanceof NullValueNode) {
return [ 'Expected "' . Utils::printSafe($type) . '", found null.' ];
}
return static::isValidLiteralValue($type->getWrappedType(), $valueNode);
}
$emptySchema = new Schema([]);
$emptyDoc = new DocumentNode(['definitions' => []]);
$typeInfo = new TypeInfo($emptySchema, $type);
$context = new ValidationContext($emptySchema, $emptyDoc, $typeInfo);
$validator = new ValuesOfCorrectType();
$visitor = $validator->getVisitor($context);
Visitor::visit($valueNode, Visitor::visitWithTypeInfo($typeInfo, $visitor));
if (!$valueNode || $valueNode instanceof NullValueNode) {
return [];
}
// This function only tests literals, and assumes variables will provide
// values of the correct type.
if ($valueNode instanceof VariableNode) {
return [];
}
// Lists accept a non-list value as a list of one.
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
if ($valueNode instanceof ListValueNode) {
$errors = [];
foreach($valueNode->values as $index => $itemNode) {
$tmp = static::isValidLiteralValue($itemType, $itemNode);
if ($tmp) {
$errors = array_merge($errors, Utils::map($tmp, function($error) use ($index) {
return "In element #$index: $error";
}));
}
}
return $errors;
} else {
return static::isValidLiteralValue($itemType, $valueNode);
}
}
// Input objects check each defined field and look for undefined fields.
if ($type instanceof InputObjectType) {
if ($valueNode->kind !== NodeKind::OBJECT) {
return [ "Expected \"{$type->name}\", found not an object." ];
}
$fields = $type->getFields();
$errors = [];
// Ensure every provided field is defined.
$fieldNodes = $valueNode->fields;
foreach ($fieldNodes as $providedFieldNode) {
if (empty($fields[$providedFieldNode->name->value])) {
$errors[] = "In field \"{$providedFieldNode->name->value}\": Unknown field.";
}
}
// Ensure every defined field is valid.
$fieldNodeMap = Utils::keyMap($fieldNodes, function($fieldNode) {return $fieldNode->name->value;});
foreach ($fields as $fieldName => $field) {
$result = static::isValidLiteralValue(
$field->getType(),
isset($fieldNodeMap[$fieldName]) ? $fieldNodeMap[$fieldName]->value : null
);
if ($result) {
$errors = array_merge($errors, Utils::map($result, function($error) use ($fieldName) {
return "In field \"$fieldName\": $error";
}));
}
}
return $errors;
}
if ($type instanceof LeafType) {
// Scalars must parse to a non-null value
if (!$type->isValidLiteral($valueNode)) {
$printed = Printer::doPrint($valueNode);
return [ "Expected type \"{$type->name}\", found $printed." ];
}
return [];
}
throw new InvariantViolation('Must be input type');
return $context->getErrors();
}
/**
* This uses a specialized visitor which runs multiple visitors in parallel,
* while maintaining the visitor skip and break API.
* @param ValidationRule[]|null $rules
*
* @param Schema $schema
* @param TypeInfo $typeInfo
* @param DocumentNode $documentNode
* @param AbstractValidationRule[] $rules
* @return array
* @return Error[]
*
* @throws Exception
*/
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);
public static function validateSDL(
DocumentNode $documentAST,
?Schema $schemaToExtend = null,
?array $rules = null
) {
$usedRules = $rules ?? self::sdlRules();
$context = new SDLValidationContext($documentAST, $schemaToExtend);
$visitors = [];
foreach ($usedRules as $rule) {
$visitors[] = $rule->getSDLVisitor($context);
}
Visitor::visit($documentNode, Visitor::visitWithTypeInfo($typeInfo, Visitor::visitInParallel($visitors)));
Visitor::visit($documentAST, Visitor::visitInParallel($visitors));
return $context->getErrors();
}
public static function assertValidSDL(DocumentNode $documentAST)
{
$errors = self::validateSDL($documentAST);
if (count($errors) > 0) {
throw new Error(self::combineErrorMessages($errors));
}
}
public static function assertValidSDLExtension(DocumentNode $documentAST, Schema $schema)
{
$errors = self::validateSDL($documentAST, $schema);
if (count($errors) > 0) {
throw new Error(self::combineErrorMessages($errors));
}
}
/**
* @param Error[] $errors
*/
private static function combineErrorMessages(array $errors) : string
{
$str = '';
foreach ($errors as $error) {
$str .= ($error->getMessage() . "\n\n");
}
return $str;
}
}
@@ -1,28 +0,0 @@
<?php
namespace GraphQL\Validator\Rules;
use GraphQL\Validator\ValidationContext;
abstract class AbstractValidationRule
{
protected $name;
public function getName()
{
return $this->name ?: get_class($this);
}
public function __invoke(ValidationContext $context)
{
return $this->getVisitor($context);
}
/**
* Returns structure suitable for GraphQL\Language\Visitor
*
* @see \GraphQL\Language\Visitor
* @param ValidationContext $context
* @return array
*/
abstract public function getVisitor(ValidationContext $context);
}
@@ -1,39 +0,0 @@
<?php
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Printer;
use GraphQL\Language\Visitor;
use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\ValidationContext;
class ArgumentsOfCorrectType extends AbstractValidationRule
{
static function badValueMessage($argName, $type, $value, $verboseErrors = [])
{
$message = $verboseErrors ? ("\n" . implode("\n", $verboseErrors)) : '';
return "Argument \"$argName\" has invalid value $value.$message";
}
public function getVisitor(ValidationContext $context)
{
return [
NodeKind::ARGUMENT => function(ArgumentNode $argNode) use ($context) {
$argDef = $context->getArgument();
if ($argDef) {
$errors = DocumentValidator::isValidLiteralValue($argDef->getType(), $argNode->value);
if (!empty($errors)) {
$context->reportError(new Error(
self::badValueMessage($argNode->name->value, $argDef->getType(), Printer::doPrint($argNode->value), $errors),
[$argNode->value]
));
}
}
return Visitor::skipNode();
}
];
}
}
@@ -1,27 +1,30 @@
<?php
namespace GraphQL\Validator\Rules;
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)
{
$this->name = $name;
$this->name = $name;
$this->visitorFn = $visitorFn;
}
/**
* @param ValidationContext $context
* @return Error[]
*/
public function getVisitor(ValidationContext $context)
{
$fn = $this->visitorFn;
return $fn($context);
}
}
@@ -1,59 +0,0 @@
<?php
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\Language\Visitor;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\ValidationContext;
class DefaultValuesOfCorrectType extends AbstractValidationRule
{
static function badValueForDefaultArgMessage($varName, $type, $value, $verboseErrors = null)
{
$message = $verboseErrors ? ("\n" . implode("\n", $verboseErrors)) : '';
return "Variable \$$varName has invalid default value: $value.$message";
}
static function defaultForNonNullArgMessage($varName, $type, $guessType)
{
return "Variable \$$varName of type $type " .
"is required and will never use the default value. " .
"Perhaps you meant to use type $guessType.";
}
public function getVisitor(ValidationContext $context)
{
return [
NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $varDefNode) use ($context) {
$name = $varDefNode->variable->name->value;
$defaultValue = $varDefNode->defaultValue;
$type = $context->getInputType();
if ($type instanceof NonNull && $defaultValue) {
$context->reportError(new Error(
static::defaultForNonNullArgMessage($name, $type, $type->getWrappedType()),
[$defaultValue]
));
}
if ($type && $defaultValue) {
$errors = DocumentValidator::isValidLiteralValue($type, $defaultValue);
if (!empty($errors)) {
$context->reportError(new Error(
static::badValueForDefaultArgMessage($name, $type, Printer::doPrint($defaultValue), $errors),
[$defaultValue]
));
}
}
return Visitor::skipNode();
},
NodeKind::SELECTION_SET => function() {return Visitor::skipNode();},
NodeKind::FRAGMENT_DEFINITION => function() {return Visitor::skipNode();}
];
}
}
@@ -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,30 +26,32 @@ class DisableIntrospection extends AbstractQuerySecurity
$this->isEnabled = $enabled;
}
static function introspectionDisabledMessage()
public function getVisitor(ValidationContext $context)
{
return $this->invokeIfNeeded(
$context,
[
NodeKind::FIELD => static function (FieldNode $node) use ($context) : void {
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';
}
protected function isEnabled()
{
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]
));
}
}
]
);
return $this->isEnabled !== self::DISABLED;
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\ExecutableDefinitionNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\TypeSystemDefinitionNode;
use GraphQL\Language\Visitor;
use GraphQL\Language\VisitorOperation;
use GraphQL\Validator\ValidationContext;
use function sprintf;
/**
* Executable definitions
*
* A GraphQL document is only valid for execution if all definitions are either
* operation or fragment definitions.
*/
class ExecutableDefinitions extends ValidationRule
{
public function getVisitor(ValidationContext $context)
{
return [
NodeKind::DOCUMENT => static function (DocumentNode $node) use ($context) : VisitorOperation {
/** @var ExecutableDefinitionNode|TypeSystemDefinitionNode $definition */
foreach ($node->definitions as $definition) {
if ($definition instanceof ExecutableDefinitionNode) {
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);
}
}
@@ -1,49 +1,168 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;
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 count;
use function sprintf;
class FieldsOnCorrectType extends AbstractValidationRule
class FieldsOnCorrectType extends ValidationRule
{
static function undefinedFieldMessage($field, $type, array $suggestedTypes = [])
{
$message = 'Cannot query field "' . $field . '" on type "' . $type.'".';
$maxLength = 5;
$count = count($suggestedTypes);
if ($count > 0) {
$suggestions = array_slice($suggestedTypes, 0, $maxLength);
$suggestions = Utils::map($suggestions, function($t) { return "\"$t\""; });
$suggestions = implode(', ', $suggestions);
if ($count > $maxLength) {
$suggestions .= ', and ' . ($count - $maxLength) . ' other types';
}
$message .= " However, this field exists on $suggestions.";
$message .= ' Perhaps you meant to use an inline fragment?';
}
return $message;
}
public function getVisitor(ValidationContext $context)
{
return [
NodeKind::FIELD => function(FieldNode $node) use ($context) {
NodeKind::FIELD => function (FieldNode $node) use ($context) : void {
$type = $context->getParentType();
if ($type) {
$fieldDef = $context->getFieldDef();
if (!$fieldDef) {
$context->reportError(new Error(
static::undefinedFieldMessage($node->name->value, $type->name),
[$node]
));
}
if (! $type) {
return;
}
}
$fieldDef = $context->getFieldDef();
if ($fieldDef) {
return;
}
// This isn't valid. Let's find suggestions, if any.
$schema = $context->getSchema();
$fieldName = $node->name->value;
// First determine if there are any suggested types to condition on.
$suggestedTypeNames = $this->getSuggestedTypeNames(
$schema,
$type,
$fieldName
);
// If there are no suggested types, then perhaps this was a typo?
$suggestedFieldNames = $suggestedTypeNames
? []
: $this->getSuggestedFieldNames(
$schema,
$type,
$fieldName
);
// Report an error, including helpful suggestions.
$context->reportError(new Error(
static::undefinedFieldMessage(
$node->name->value,
$type->name,
$suggestedTypeNames,
$suggestedFieldNames
),
[$node]
));
},
];
}
/**
* Go through all of the implementations of type, as well as the interfaces
* that they implement. If any of those types include the provided field,
* suggest them, sorted by how often the type is referenced, starting
* with Interfaces.
*
* @param ObjectType|InterfaceType $type
* @param string $fieldName
*
* @return string[]
*/
private function getSuggestedTypeNames(Schema $schema, $type, $fieldName)
{
if (Type::isAbstractType($type)) {
$suggestedObjectTypes = [];
$interfaceUsageCount = [];
foreach ($schema->getPossibleTypes($type) as $possibleType) {
$fields = $possibleType->getFields();
if (! isset($fields[$fieldName])) {
continue;
}
// This object type defines this field.
$suggestedObjectTypes[] = $possibleType->name;
foreach ($possibleType->getInterfaces() as $possibleInterface) {
$fields = $possibleInterface->getFields();
if (! isset($fields[$fieldName])) {
continue;
}
// This interface type defines this field.
$interfaceUsageCount[$possibleInterface->name] =
! isset($interfaceUsageCount[$possibleInterface->name])
? 0
: $interfaceUsageCount[$possibleInterface->name] + 1;
}
}
// Suggest interface types based on how common they are.
arsort($interfaceUsageCount);
$suggestedInterfaceTypes = array_keys($interfaceUsageCount);
// Suggest both interface and object types.
return array_merge($suggestedInterfaceTypes, $suggestedObjectTypes);
}
// Otherwise, must be an Object type, which does not have possible fields.
return [];
}
/**
* For the field name provided, determine if there are any similar field names
* that may be the result of a typo.
*
* @param ObjectType|InterfaceType $type
* @param string $fieldName
*
* @return array|string[]
*/
private function getSuggestedFieldNames(Schema $schema, $type, $fieldName)
{
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
$possibleFieldNames = array_keys($type->getFields());
return Utils::suggestionList($fieldName, $possibleFieldNames);
}
// Otherwise, must be a Union type, which does not define fields.
return [];
}
/**
* @param string $fieldName
* @param string $type
* @param string[] $suggestedTypeNames
* @param string[] $suggestedFieldNames
*
* @return string
*/
public static function undefinedFieldMessage(
$fieldName,
$type,
array $suggestedTypeNames,
array $suggestedFieldNames
) {
$message = sprintf('Cannot query field "%s" on type "%s".', $fieldName, $type);
if ($suggestedTypeNames) {
$suggestions = Utils::quotedOrList($suggestedTypeNames);
$message .= sprintf(' Did you mean to use an inline fragment on %s?', $suggestions);
} elseif (count($suggestedFieldNames) > 0) {
$suggestions = Utils::quotedOrList($suggestedFieldNames);
$message .= sprintf(' Did you mean %s?', $suggestions);
}
return $message;
}
}
@@ -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) {
$type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition);
if ($type && !Type::isCompositeType($type)) {
$context->reportError(new Error(
static::inlineFragmentOnNonCompositeErrorMessage($type),
[$node->typeCondition]
));
}
NodeKind::INLINE_FRAGMENT => static function (InlineFragmentNode $node) use ($context) : void {
if (! $node->typeCondition) {
return;
}
$type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition);
if (! $type || Type::isCompositeType($type)) {
return;
}
$context->reportError(new Error(
static::inlineFragmentOnNonCompositeErrorMessage($type),
[$node->typeCondition]
));
},
NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) use ($context) {
NodeKind::FRAGMENT_DEFINITION => static function (FragmentDefinitionNode $node) use ($context) : void {
$type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition);
if ($type && !Type::isCompositeType($type)) {
$context->reportError(new Error(
static::fragmentOnNonCompositeErrorMessage($node->name->value, Printer::doPrint($node->typeCondition)),
[$node->typeCondition]
));
if (! $type || Type::isCompositeType($type)) {
return;
}
}
$context->reportError(new Error(
static::fragmentOnNonCompositeErrorMessage(
$node->name->value,
Printer::doPrint($node->typeCondition)
),
[$node->typeCondition]
));
},
];
}
public static function inlineFragmentOnNonCompositeErrorMessage($type)
{
return sprintf('Fragment cannot condition on non composite type "%s".', $type);
}
public static function fragmentOnNonCompositeErrorMessage($fragName, $type)
{
return sprintf('Fragment "%s" cannot condition on non composite type "%s".', $fragName, $type);
}
}
@@ -1,68 +1,80 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext;
use function array_map;
use function count;
use function sprintf;
class KnownArgumentNames extends AbstractValidationRule
/**
* Known argument names
*
* A GraphQL field is only valid if all supplied arguments are defined by
* that field.
*/
class KnownArgumentNames extends ValidationRule
{
public static function unknownArgMessage($argName, $fieldName, $type)
{
return "Unknown argument \"$argName\" on field \"$fieldName\" of type \"$type\".";
}
public static function unknownDirectiveArgMessage($argName, $directiveName)
{
return "Unknown argument \"$argName\" on directive \"@$directiveName\".";
}
public function getVisitor(ValidationContext $context)
{
return [
NodeKind::ARGUMENT => function(ArgumentNode $node, $key, $parent, $path, $ancestors) use ($context) {
$argumentOf = $ancestors[count($ancestors) - 1];
if ($argumentOf->kind === NodeKind::FIELD) {
$fieldDef = $context->getFieldDef();
$knownArgumentNamesOnDirectives = new KnownArgumentNamesOnDirectives();
if ($fieldDef) {
$fieldArgDef = null;
foreach ($fieldDef->args as $arg) {
if ($arg->name === $node->name->value) {
$fieldArgDef = $arg;
break;
}
}
if (!$fieldArgDef) {
$parentType = $context->getParentType();
Utils::invariant($parentType);
$context->reportError(new Error(
self::unknownArgMessage($node->name->value, $fieldDef->name, $parentType->name),
[$node]
));
}
}
} else if ($argumentOf->kind === NodeKind::DIRECTIVE) {
$directive = $context->getDirective();
if ($directive) {
$directiveArgDef = null;
foreach ($directive->args as $arg) {
if ($arg->name === $node->name->value) {
$directiveArgDef = $arg;
break;
}
}
if (!$directiveArgDef) {
$context->reportError(new Error(
self::unknownDirectiveArgMessage($node->name->value, $directive->name),
[$node]
));
}
}
return $knownArgumentNamesOnDirectives->getVisitor($context) + [
NodeKind::ARGUMENT => static function (ArgumentNode $node) use ($context) : void {
$argDef = $context->getArgument();
if ($argDef !== null) {
return;
}
}
$fieldDef = $context->getFieldDef();
$parentType = $context->getParentType();
if ($fieldDef === null || ! ($parentType instanceof Type)) {
return;
}
$context->reportError(new Error(
self::unknownArgMessage(
$node->name->value,
$fieldDef->name,
$parentType->name,
Utils::suggestionList(
$node->name->value,
array_map(
static function ($arg) : string {
return $arg->name;
},
$fieldDef->args
)
)
),
[$node]
));
return;
},
];
}
/**
* @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 (isset($suggestedArgs[0])) {
$message .= sprintf(' Did you mean %s?', Utils::quotedOrList($suggestedArgs));
}
return $message;
}
}
@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Visitor;
use GraphQL\Language\VisitorOperation;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Utils\Utils;
use GraphQL\Validator\ASTValidationContext;
use GraphQL\Validator\SDLValidationContext;
use GraphQL\Validator\ValidationContext;
use function array_map;
use function in_array;
use function sprintf;
/**
* Known argument names on directives
*
* A GraphQL directive is only valid if all supplied arguments are defined by
* that field.
*/
class KnownArgumentNamesOnDirectives extends ValidationRule
{
/**
* @param string[] $suggestedArgs
*/
public static function unknownDirectiveArgMessage($argName, $directiveName, array $suggestedArgs)
{
$message = sprintf('Unknown argument "%s" on directive "@%s".', $argName, $directiveName);
if (isset($suggestedArgs[0])) {
$message .= sprintf(' Did you mean %s?', Utils::quotedOrList($suggestedArgs));
}
return $message;
}
public function getSDLVisitor(SDLValidationContext $context)
{
return $this->getASTVisitor($context);
}
public function getVisitor(ValidationContext $context)
{
return $this->getASTVisitor($context);
}
public function getASTVisitor(ASTValidationContext $context)
{
$directiveArgs = [];
$schema = $context->getSchema();
$definedDirectives = $schema !== null ? $schema->getDirectives() : Directive::getInternalDirectives();
foreach ($definedDirectives as $directive) {
$directiveArgs[$directive->name] = array_map(
static function (FieldArgument $arg) : string {
return $arg->name;
},
$directive->args
);
}
$astDefinitions = $context->getDocument()->definitions;
foreach ($astDefinitions as $def) {
if (! ($def instanceof DirectiveDefinitionNode)) {
continue;
}
$name = $def->name->value;
if ($def->arguments !== null) {
$directiveArgs[$name] = Utils::map(
$def->arguments ?? [],
static function (InputValueDefinitionNode $arg) : string {
return $arg->name->value;
}
);
} else {
$directiveArgs[$name] = [];
}
}
return [
NodeKind::DIRECTIVE => static function (DirectiveNode $directiveNode) use ($directiveArgs, $context) : VisitorOperation {
$directiveName = $directiveNode->name->value;
$knownArgs = $directiveArgs[$directiveName] ?? null;
if ($directiveNode->arguments === null || $knownArgs === null) {
return Visitor::skipNode();
}
foreach ($directiveNode->arguments as $argNode) {
$argName = $argNode->name->value;
if (in_array($argName, $knownArgs, true)) {
continue;
}
$suggestions = Utils::suggestionList($argName, $knownArgs);
$context->reportError(new Error(
self::unknownDirectiveArgMessage($argName, $directiveName, $suggestions),
[$argNode]
));
}
return Visitor::skipNode();
},
];
}
}
@@ -1,77 +1,200 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use Exception;
use GraphQL\Error\Error;
use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\EnumTypeDefinitionNode;
use GraphQL\Language\AST\EnumTypeExtensionNode;
use GraphQL\Language\AST\EnumValueDefinitionNode;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
use GraphQL\Language\AST\InputObjectTypeExtensionNode;
use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Language\AST\ObjectTypeExtensionNode;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
use GraphQL\Language\AST\ScalarTypeExtensionNode;
use GraphQL\Language\AST\SchemaDefinitionNode;
use GraphQL\Language\AST\SchemaTypeExtensionNode;
use GraphQL\Language\AST\UnionTypeDefinitionNode;
use GraphQL\Language\AST\UnionTypeExtensionNode;
use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Language\DirectiveLocation;
use GraphQL\Type\Definition\Directive;
use GraphQL\Utils\Utils;
use GraphQL\Validator\ASTValidationContext;
use GraphQL\Validator\SDLValidationContext;
use GraphQL\Validator\ValidationContext;
use GraphQL\Type\Definition\DirectiveLocation;
use function array_map;
use function count;
use function get_class;
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 [
NodeKind::DIRECTIVE => function (DirectiveNode $node, $key, $parent, $path, $ancestors) use ($context) {
$directiveDef = null;
foreach ($context->getSchema()->getDirectives() as $def) {
if ($def->name === $node->name->value) {
$directiveDef = $def;
break;
}
}
return $this->getASTVisitor($context);
}
if (!$directiveDef) {
$context->reportError(new Error(
self::unknownDirectiveMessage($node->name->value),
[$node]
));
return ;
}
$appliedTo = $ancestors[count($ancestors) - 1];
$candidateLocation = $this->getLocationForAppliedNode($appliedTo);
public function getSDLVisitor(SDLValidationContext $context)
{
return $this->getASTVisitor($context);
}
if (!$candidateLocation) {
$context->reportError(new Error(
self::misplacedDirectiveMessage($node->name->value, $node->type),
[$node]
));
} else if (!in_array($candidateLocation, $directiveDef->locations)) {
$context->reportError(new Error(
self::misplacedDirectiveMessage($node->name->value, $candidateLocation),
[ $node ]
));
}
public function getASTVisitor(ASTValidationContext $context)
{
$locationsMap = [];
$schema = $context->getSchema();
$definedDirectives = $schema
? $schema->getDirectives()
: Directive::getInternalDirectives();
foreach ($definedDirectives as $directive) {
$locationsMap[$directive->name] = $directive->locations;
}
$astDefinition = $context->getDocument()->definitions;
foreach ($astDefinition as $def) {
if (! ($def instanceof DirectiveDefinitionNode)) {
continue;
}
$locationsMap[$def->name->value] = Utils::map(
$def->locations,
static function ($name) : string {
return $name->value;
}
);
}
return [
NodeKind::DIRECTIVE => function (
DirectiveNode $node,
$key,
$parent,
$path,
$ancestors
) use (
$context,
$locationsMap
) : void {
$name = $node->name->value;
$locations = $locationsMap[$name] ?? null;
if (! $locations) {
$context->reportError(new Error(
self::unknownDirectiveMessage($name),
[$node]
));
return;
}
$candidateLocation = $this->getDirectiveLocationForASTPath($ancestors);
if (! $candidateLocation || in_array($candidateLocation, $locations, true)) {
return;
}
$context->reportError(
new Error(
self::misplacedDirectiveMessage($name, $candidateLocation),
[$node]
)
);
},
];
}
private function getLocationForAppliedNode(Node $appliedTo)
public static function unknownDirectiveMessage($directiveName)
{
switch ($appliedTo->kind) {
case NodeKind::OPERATION_DEFINITION:
return sprintf('Unknown directive "%s".', $directiveName);
}
/**
* @param Node[]|NodeList[] $ancestors The type is actually (Node|NodeList)[] but this PSR-5 syntax is so far not supported by most of the tools
*
* @return string
*/
private function getDirectiveLocationForASTPath(array $ancestors)
{
$appliedTo = $ancestors[count($ancestors) - 1];
switch (true) {
case $appliedTo instanceof OperationDefinitionNode:
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: return DirectiveLocation::FIELD;
case NodeKind::FRAGMENT_SPREAD: return DirectiveLocation::FRAGMENT_SPREAD;
case NodeKind::INLINE_FRAGMENT: return DirectiveLocation::INLINE_FRAGMENT;
case NodeKind::FRAGMENT_DEFINITION: return DirectiveLocation::FRAGMENT_DEFINITION;
case $appliedTo instanceof FieldNode:
return DirectiveLocation::FIELD;
case $appliedTo instanceof FragmentSpreadNode:
return DirectiveLocation::FRAGMENT_SPREAD;
case $appliedTo instanceof InlineFragmentNode:
return DirectiveLocation::INLINE_FRAGMENT;
case $appliedTo instanceof FragmentDefinitionNode:
return DirectiveLocation::FRAGMENT_DEFINITION;
case $appliedTo instanceof VariableDefinitionNode:
return DirectiveLocation::VARIABLE_DEFINITION;
case $appliedTo instanceof SchemaDefinitionNode:
case $appliedTo instanceof SchemaTypeExtensionNode:
return DirectiveLocation::SCHEMA;
case $appliedTo instanceof ScalarTypeDefinitionNode:
case $appliedTo instanceof ScalarTypeExtensionNode:
return DirectiveLocation::SCALAR;
case $appliedTo instanceof ObjectTypeDefinitionNode:
case $appliedTo instanceof ObjectTypeExtensionNode:
return DirectiveLocation::OBJECT;
case $appliedTo instanceof FieldDefinitionNode:
return DirectiveLocation::FIELD_DEFINITION;
case $appliedTo instanceof InterfaceTypeDefinitionNode:
case $appliedTo instanceof InterfaceTypeExtensionNode:
return DirectiveLocation::IFACE;
case $appliedTo instanceof UnionTypeDefinitionNode:
case $appliedTo instanceof UnionTypeExtensionNode:
return DirectiveLocation::UNION;
case $appliedTo instanceof EnumTypeDefinitionNode:
case $appliedTo instanceof EnumTypeExtensionNode:
return DirectiveLocation::ENUM;
case $appliedTo instanceof EnumValueDefinitionNode:
return DirectiveLocation::ENUM_VALUE;
case $appliedTo instanceof InputObjectTypeDefinitionNode:
case $appliedTo instanceof InputObjectTypeExtensionNode:
return DirectiveLocation::INPUT_OBJECT;
case $appliedTo instanceof InputValueDefinitionNode:
$parentNode = $ancestors[count($ancestors) - 3];
return $parentNode instanceof InputObjectTypeDefinitionNode
? DirectiveLocation::INPUT_FIELD_DEFINITION
: DirectiveLocation::ARGUMENT_DEFINITION;
}
throw new Exception('Unknown directive location: ' . get_class($appliedTo));
}
public static function misplacedDirectiveMessage($directiveName, $location)
{
return sprintf('Directive "%s" may not be used on "%s".', $directiveName, $location);
}
}
@@ -1,33 +1,40 @@
<?php
namespace GraphQL\Validator\Rules;
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\Node;
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 => static function (FragmentSpreadNode $node) use ($context) : void {
$fragmentName = $node->name->value;
$fragment = $context->getFragment($fragmentName);
if (!$fragment) {
$context->reportError(new Error(
self::unknownFragmentMessage($fragmentName),
[$node->name]
));
$fragment = $context->getFragment($fragmentName);
if ($fragment) {
return;
}
}
$context->reportError(new Error(
self::unknownFragmentMessage($fragmentName),
[$node->name]
));
},
];
}
/**
* @param string $fragName
*/
public static function unknownFragmentMessage($fragName)
{
return sprintf('Unknown fragment "%s".', $fragName);
}
}
@@ -1,37 +1,74 @@
<?php
namespace GraphQL\Validator\Rules;
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\NamedTypeNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Visitor;
use GraphQL\Language\VisitorOperation;
use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext;
use function array_keys;
use function count;
use function sprintf;
class KnownTypeNames extends AbstractValidationRule
/**
* Known type names
*
* A GraphQL document is only valid if referenced types (specifically
* variable definitions and fragment conditions) are defined by the type schema.
*/
class KnownTypeNames extends ValidationRule
{
static function unknownTypeMessage($type)
{
return "Unknown type \"$type\".";
}
public function getVisitor(ValidationContext $context)
{
$skip = function() {return Visitor::skipNode();};
$skip = static function () : VisitorOperation {
return Visitor::skipNode();
};
return [
NodeKind::OBJECT_TYPE_DEFINITION => $skip,
NodeKind::INTERFACE_TYPE_DEFINITION => $skip,
NodeKind::UNION_TYPE_DEFINITION => $skip,
// TODO: when validating IDL, re-enable these. Experimental version does not
// add unreferenced types, resulting in false-positive errors. Squelched
// errors for now.
NodeKind::OBJECT_TYPE_DEFINITION => $skip,
NodeKind::INTERFACE_TYPE_DEFINITION => $skip,
NodeKind::UNION_TYPE_DEFINITION => $skip,
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $skip,
NodeKind::NAMED_TYPE => function(NamedTypeNode $node, $key) use ($context) {
NodeKind::NAMED_TYPE => static function (NamedTypeNode $node) use ($context) : void {
$schema = $context->getSchema();
$typeName = $node->name->value;
$type = $context->getSchema()->getType($typeName);
if (!$type) {
$context->reportError(new Error(self::unknownTypeMessage($typeName), [$node]));
$type = $schema->getType($typeName);
if ($type !== null) {
return;
}
}
$context->reportError(new Error(
self::unknownTypeMessage(
$typeName,
Utils::suggestionList($typeName, array_keys($schema->getTypeMap()))
),
[$node]
));
},
];
}
/**
* @param string $type
* @param string[] $suggestedTypes
*/
public static function unknownTypeMessage($type, array $suggestedTypes)
{
$message = sprintf('Unknown type "%s".', $type);
if (count($suggestedTypes) > 0) {
$suggestions = Utils::quotedOrList($suggestedTypes);
$message .= sprintf(' Did you mean %s?', $suggestions);
}
return $message;
}
}
@@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
@@ -8,6 +11,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
@@ -15,33 +19,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 => static function (DocumentNode $node) use (&$operationCount) : void {
$tmp = Utils::filter(
$node->definitions,
function ($definition) {
return $definition->kind === NodeKind::OPERATION_DEFINITION;
static function (Node $definition) : bool {
return $definition instanceof OperationDefinitionNode;
}
);
$operationCount = count($tmp);
},
NodeKind::OPERATION_DEFINITION => function(OperationDefinitionNode $node) use (&$operationCount, $context) {
if (!$node->name && $operationCount > 1) {
$context->reportError(
new Error(self::anonOperationNotAloneMessage(), [$node])
);
NodeKind::OPERATION_DEFINITION => static function (OperationDefinitionNode $node) use (
&$operationCount,
$context
) : void {
if ($node->name !== null || $operationCount <= 1) {
return;
}
}
$context->reportError(
new Error(self::anonOperationNotAloneMessage(), [$node])
);
},
];
}
public static function anonOperationNotAloneMessage()
{
return 'This anonymous operation must be the only defined operation.';
}
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\SchemaDefinitionNode;
use GraphQL\Validator\SDLValidationContext;
/**
* Lone Schema definition
*
* A GraphQL document is only valid if it contains only one schema definition.
*/
class LoneSchemaDefinition extends ValidationRule
{
public static function schemaDefinitionNotAloneMessage()
{
return 'Must provide only one schema definition.';
}
public static function canNotDefineSchemaWithinExtensionMessage()
{
return 'Cannot define a new schema within a schema extension.';
}
public function getSDLVisitor(SDLValidationContext $context)
{
$oldSchema = $context->getSchema();
$alreadyDefined = $oldSchema !== null
? (
$oldSchema->getAstNode() !== null ||
$oldSchema->getQueryType() !== null ||
$oldSchema->getMutationType() !== null ||
$oldSchema->getSubscriptionType() !== null
)
: false;
$schemaDefinitionsCount = 0;
return [
NodeKind::SCHEMA_DEFINITION => static function (SchemaDefinitionNode $node) use ($alreadyDefined, $context, &$schemaDefinitionsCount) : void {
if ($alreadyDefined !== false) {
$context->reportError(new Error(self::canNotDefineSchemaWithinExtensionMessage(), $node));
return;
}
if ($schemaDefinitionsCount > 0) {
$context->reportError(new Error(self::schemaDefinitionNotAloneMessage(), $node));
}
++$schemaDefinitionsCount;
},
];
}
}
@@ -1,28 +1,32 @@
<?php
namespace GraphQL\Validator\Rules;
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\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Visitor;
use GraphQL\Language\VisitorOperation;
use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext;
use function array_pop;
use function array_slice;
use function count;
use function implode;
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)
@@ -38,26 +42,29 @@ class NoFragmentCycles extends AbstractValidationRule
$this->spreadPathIndexByName = [];
return [
NodeKind::OPERATION_DEFINITION => function () {
NodeKind::OPERATION_DEFINITION => static function () : VisitorOperation {
return Visitor::skipNode();
},
NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) {
if (!isset($this->visitedFrags[$node->name->value])) {
$this->detectCycleRecursive($node, $context);
}
NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) : VisitorOperation {
$this->detectCycleRecursive($node, $context);
return Visitor::skipNode();
}
},
];
}
private function detectCycleRecursive(FragmentDefinitionNode $fragment, ValidationContext $context)
{
$fragmentName = $fragment->name->value;
if (isset($this->visitedFrags[$fragment->name->value])) {
return;
}
$fragmentName = $fragment->name->value;
$this->visitedFrags[$fragmentName] = true;
$spreadNodes = $context->getFragmentSpreads($fragment);
if (empty($spreadNodes)) {
if (count($spreadNodes) === 0) {
return;
}
@@ -66,39 +73,40 @@ 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;
$this->spreadPath[] = $spreadNode;
if ($cycleIndex === null) {
$this->spreadPath[] = $spreadNode;
if (empty($this->visitedFrags[$spreadName])) {
$spreadFragment = $context->getFragment($spreadName);
if ($spreadFragment) {
$this->detectCycleRecursive($spreadFragment, $context);
}
$spreadFragment = $context->getFragment($spreadName);
if ($spreadFragment) {
$this->detectCycleRecursive($spreadFragment, $context);
}
array_pop($this->spreadPath);
} else {
$cyclePath = array_slice($this->spreadPath, $cycleIndex);
$nodes = $cyclePath;
if (is_array($spreadNode)) {
$nodes = array_merge($nodes, $spreadNode);
} else {
$nodes[] = $spreadNode;
}
$cyclePath = array_slice($this->spreadPath, $cycleIndex);
$fragmentNames = Utils::map(array_slice($cyclePath, 0, -1), static function ($s) {
return $s->name->value;
});
$context->reportError(new Error(
self::cycleErrorMessage(
$spreadName,
Utils::map($cyclePath, function ($s) {
return $s->name->value;
})
),
$nodes
self::cycleErrorMessage($spreadName, $fragmentNames),
$cyclePath
));
}
array_pop($this->spreadPath);
}
$this->spreadPathIndexByName[$fragmentName] = null;
}
/**
* @param string[] $spreadNames
*/
public static function cycleErrorMessage($fragName, array $spreadNames = [])
{
return sprintf(
'Cannot spread fragment "%s" within itself%s.',
$fragName,
count($spreadNames) > 0 ? ' via ' . implode(', ', $spreadNames) : ''
);
}
}
@@ -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' => static function () use (&$variableNameDefined) : void {
$variableNameDefined = [];
},
'leave' => function(OperationDefinitionNode $operation) use (&$variableNameDefined, $context) {
'leave' => static function (OperationDefinitionNode $operation) use (&$variableNameDefined, $context) : void {
$usages = $context->getRecursiveVariableUsages($operation);
foreach ($usages as $usage) {
$node = $usage['node'];
$node = $usage['node'];
$varName = $node->name->value;
if (empty($variableNameDefined[$varName])) {
$context->reportError(new Error(
self::undefinedVarMessage(
$varName,
$operation->name ? $operation->name->value : null
),
[ $node, $operation ]
));
if ($variableNameDefined[$varName] ?? false) {
continue;
}
$context->reportError(new Error(
self::undefinedVarMessage(
$varName,
$operation->name !== null
? $operation->name->value
: null
),
[$node, $operation]
));
}
}
},
],
NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $def) use (&$variableNameDefined) {
NodeKind::VARIABLE_DEFINITION => static function (VariableDefinitionNode $def) use (&$variableNameDefined) : void {
$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);
}
}
@@ -1,39 +1,44 @@
<?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\Language\VisitorOperation;
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)
{
$this->operationDefs = [];
$this->fragmentDefs = [];
$this->fragmentDefs = [];
return [
NodeKind::OPERATION_DEFINITION => function($node) {
NodeKind::OPERATION_DEFINITION => function ($node) : VisitorOperation {
$this->operationDefs[] = $node;
return Visitor::skipNode();
},
NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $def) {
NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $def) : VisitorOperation {
$this->fragmentDefs[] = $def;
return Visitor::skipNode();
},
NodeKind::DOCUMENT => [
'leave' => function() use ($context) {
NodeKind::DOCUMENT => [
'leave' => function () use ($context) : void {
$fragmentNameUsed = [];
foreach ($this->operationDefs as $operation) {
@@ -44,15 +49,22 @@ class NoUnusedFragments extends AbstractValidationRule
foreach ($this->fragmentDefs as $fragmentDef) {
$fragName = $fragmentDef->name->value;
if (empty($fragmentNameUsed[$fragName])) {
$context->reportError(new Error(
self::unusedFragMessage($fragName),
[ $fragmentDef ]
));
if ($fragmentNameUsed[$fragName] ?? false) {
continue;
}
$context->reportError(new Error(
self::unusedFragMessage($fragName),
[$fragmentDef]
));
}
}
]
},
],
];
}
public static function unusedFragMessage($fragName)
{
return sprintf('Fragment "%s" is never used.', $fragName);
}
}
@@ -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,34 +22,45 @@ class NoUnusedVariables extends AbstractValidationRule
return [
NodeKind::OPERATION_DEFINITION => [
'enter' => function() {
'enter' => function () : void {
$this->variableDefs = [];
},
'leave' => function(OperationDefinitionNode $operation) use ($context) {
'leave' => function (OperationDefinitionNode $operation) use ($context) : void {
$variableNameUsed = [];
$usages = $context->getRecursiveVariableUsages($operation);
$opName = $operation->name ? $operation->name->value : null;
$usages = $context->getRecursiveVariableUsages($operation);
$opName = $operation->name !== null
? $operation->name->value
: null;
foreach ($usages as $usage) {
$node = $usage['node'];
$node = $usage['node'];
$variableNameUsed[$node->name->value] = true;
}
foreach ($this->variableDefs as $variableDef) {
$variableName = $variableDef->variable->name->value;
if (empty($variableNameUsed[$variableName])) {
$context->reportError(new Error(
self::unusedVariableMessage($variableName, $opName),
[$variableDef]
));
if ($variableNameUsed[$variableName] ?? false) {
continue;
}
$context->reportError(new Error(
self::unusedVariableMessage($variableName, $opName),
[$variableDef]
));
}
}
},
],
NodeKind::VARIABLE_DEFINITION => function($def) {
NodeKind::VARIABLE_DEFINITION => function ($def) : void {
$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
@@ -1,67 +1,61 @@
<?php
namespace GraphQL\Validator\Rules;
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) {
$fragType = $context->getType();
NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) use ($context) : void {
$fragType = $context->getType();
$parentType = $context->getParentType();
if ($fragType instanceof CompositeType &&
$parentType instanceof CompositeType &&
!$this->doTypesOverlap($context->getSchema(), $fragType, $parentType)) {
$context->reportError(new Error(
self::typeIncompatibleAnonSpreadMessage($parentType, $fragType),
[$node]
));
if (! ($fragType instanceof CompositeType) ||
! ($parentType instanceof CompositeType) ||
$this->doTypesOverlap($context->getSchema(), $fragType, $parentType)) {
return;
}
$context->reportError(new Error(
self::typeIncompatibleAnonSpreadMessage($parentType, $fragType),
[$node]
));
},
NodeKind::FRAGMENT_SPREAD => function(FragmentSpreadNode $node) use ($context) {
$fragName = $node->name->value;
$fragType = $this->getFragmentType($context, $fragName);
NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) use ($context) : void {
$fragName = $node->name->value;
$fragType = $this->getFragmentType($context, $fragName);
$parentType = $context->getParentType();
if ($fragType && $parentType && !$this->doTypesOverlap($context->getSchema(), $fragType, $parentType)) {
$context->reportError(new Error(
self::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType),
[$node]
));
if (! $fragType ||
! $parentType ||
$this->doTypesOverlap($context->getSchema(), $fragType, $parentType)
) {
return;
}
}
];
}
private function getFragmentType(ValidationContext $context, $name)
{
$frag = $context->getFragment($name);
return $frag ? TypeInfo::typeFromAST($context->getSchema(), $frag->typeCondition) : null;
$context->reportError(new Error(
self::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType),
[$node]
));
},
];
}
private function doTypesOverlap(Schema $schema, CompositeType $fragType, CompositeType $parentType)
@@ -74,12 +68,12 @@ class PossibleFragmentSpreads extends AbstractValidationRule
// Parent type is interface or union, fragment type is object type
if ($parentType instanceof AbstractType && $fragType instanceof ObjectType) {
return $schema->isPossibleType($parentType, $fragType);
return $schema->isSubType($parentType, $fragType);
}
// Parent type is object type, fragment type is interface (or rather rare - union)
if ($parentType instanceof ObjectType && $fragType instanceof AbstractType) {
return $schema->isPossibleType($fragType, $parentType);
return $schema->isSubType($fragType, $parentType);
}
// Both are object types:
@@ -131,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
);
}
}
@@ -1,79 +0,0 @@
<?php
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Visitor;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext;
class ProvidedNonNullArguments extends AbstractValidationRule
{
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) {
$fieldDef = $context->getFieldDef();
if (!$fieldDef) {
return Visitor::skipNode();
}
$argNodes = $fieldNode->arguments ?: [];
$argNodeMap = [];
foreach ($argNodes as $argNode) {
$argNodeMap[$argNode->name->value] = $argNodes;
}
foreach ($fieldDef->args as $argDef) {
$argNode = isset($argNodeMap[$argDef->name]) ? $argNodeMap[$argDef->name] : null;
if (!$argNode && $argDef->getType() instanceof NonNull) {
$context->reportError(new Error(
self::missingFieldArgMessage($fieldNode->name->value, $argDef->name, $argDef->getType()),
[$fieldNode]
));
}
}
}
],
NodeKind::DIRECTIVE => [
'leave' => function(DirectiveNode $directiveNode) use ($context) {
$directiveDef = $context->getDirective();
if (!$directiveDef) {
return Visitor::skipNode();
}
$argNodes = $directiveNode->arguments ?: [];
$argNodeMap = [];
foreach ($argNodes as $argNode) {
$argNodeMap[$argNode->name->value] = $argNodes;
}
foreach ($directiveDef->args as $argDef) {
$argNode = isset($argNodeMap[$argDef->name]) ? $argNodeMap[$argDef->name] : null;
if (!$argNode && $argDef->getType() instanceof NonNull) {
$context->reportError(new Error(
self::missingDirectiveArgMessage($directiveNode->name->value, $argDef->name, $argDef->getType()),
[$directiveNode]
));
}
}
}
]
];
}
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Visitor;
use GraphQL\Language\VisitorOperation;
use GraphQL\Validator\ValidationContext;
use function sprintf;
class ProvidedRequiredArguments extends ValidationRule
{
public function getVisitor(ValidationContext $context)
{
$providedRequiredArgumentsOnDirectives = new ProvidedRequiredArgumentsOnDirectives();
return $providedRequiredArgumentsOnDirectives->getVisitor($context) + [
NodeKind::FIELD => [
'leave' => static function (FieldNode $fieldNode) use ($context) : ?VisitorOperation {
$fieldDef = $context->getFieldDef();
if (! $fieldDef) {
return Visitor::skipNode();
}
$argNodes = $fieldNode->arguments ?? [];
$argNodeMap = [];
foreach ($argNodes as $argNode) {
$argNodeMap[$argNode->name->value] = $argNode;
}
foreach ($fieldDef->args as $argDef) {
$argNode = $argNodeMap[$argDef->name] ?? null;
if ($argNode || ! $argDef->isRequired()) {
continue;
}
$context->reportError(new Error(
self::missingFieldArgMessage($fieldNode->name->value, $argDef->name, $argDef->getType()),
[$fieldNode]
));
}
return null;
},
],
];
}
public static function missingFieldArgMessage($fieldName, $argName, $type)
{
return sprintf(
'Field "%s" argument "%s" of type "%s" is required but not provided.',
$fieldName,
$argName,
$type
);
}
}
@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NonNullTypeNode;
use GraphQL\Language\Printer;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Utils\Utils;
use GraphQL\Validator\ASTValidationContext;
use GraphQL\Validator\SDLValidationContext;
use GraphQL\Validator\ValidationContext;
use function array_filter;
/**
* Provided required arguments on directives
*
* A directive is only valid if all required (non-null without a
* default value) field arguments have been provided.
*/
class ProvidedRequiredArgumentsOnDirectives extends ValidationRule
{
public static function missingDirectiveArgMessage(string $directiveName, string $argName, string $type)
{
return 'Directive "@' . $directiveName . '" argument "' . $argName
. '" of type "' . $type . '" is required but not provided.';
}
public function getSDLVisitor(SDLValidationContext $context)
{
return $this->getASTVisitor($context);
}
public function getVisitor(ValidationContext $context)
{
return $this->getASTVisitor($context);
}
public function getASTVisitor(ASTValidationContext $context)
{
$requiredArgsMap = [];
$schema = $context->getSchema();
$definedDirectives = $schema
? $schema->getDirectives()
: Directive::getInternalDirectives();
foreach ($definedDirectives as $directive) {
$requiredArgsMap[$directive->name] = Utils::keyMap(
array_filter($directive->args, static function (FieldArgument $arg) : bool {
return $arg->isRequired();
}),
static function (FieldArgument $arg) : string {
return $arg->name;
}
);
}
$astDefinition = $context->getDocument()->definitions;
foreach ($astDefinition as $def) {
if (! ($def instanceof DirectiveDefinitionNode)) {
continue;
}
$arguments = $def->arguments ?? [];
$requiredArgsMap[$def->name->value] = Utils::keyMap(
Utils::filter($arguments, static function (InputValueDefinitionNode $argument) : bool {
return $argument->type instanceof NonNullTypeNode &&
(
! isset($argument->defaultValue) ||
$argument->defaultValue === null
);
}),
static function (InputValueDefinitionNode $argument) : string {
return $argument->name->value;
}
);
}
return [
NodeKind::DIRECTIVE => [
// Validate on leave to allow for deeper errors to appear first.
'leave' => static function (DirectiveNode $directiveNode) use ($requiredArgsMap, $context) : ?string {
$directiveName = $directiveNode->name->value;
$requiredArgs = $requiredArgsMap[$directiveName] ?? null;
if (! $requiredArgs) {
return null;
}
$argNodes = $directiveNode->arguments ?? [];
$argNodeMap = Utils::keyMap(
$argNodes,
static function (ArgumentNode $arg) : string {
return $arg->name->value;
}
);
foreach ($requiredArgs as $argName => $arg) {
if (isset($argNodeMap[$argName])) {
continue;
}
if ($arg instanceof FieldArgument) {
$argType = (string) $arg->getType();
} elseif ($arg instanceof InputValueDefinitionNode) {
$argType = Printer::doPrint($arg->type);
} else {
$argType = '';
}
$context->reportError(
new Error(static::missingDirectiveArgMessage($directiveName, $argName, $argType), [$directiveNode])
);
}
return null;
},
],
];
}
}
@@ -1,7 +1,10 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use ArrayObject;
use GraphQL\Error\Error;
use GraphQL\Executor\Values;
use GraphQL\Language\AST\FieldNode;
@@ -12,74 +15,53 @@ use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Language\Visitor;
use GraphQL\Language\VisitorOperation;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Validator\ValidationContext;
use function array_map;
use function count;
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;
/** @var int */
private $complexity;
public function __construct($maxQueryComplexity)
{
$this->setMaxQueryComplexity($maxQueryComplexity);
}
public static function maxQueryComplexityErrorMessage($max, $count)
{
return sprintf('Max query complexity should be %d but got %d.', $max, $count);
}
/**
* Set max query complexity. If equal to 0 no check is done. Must be greater or equal to 0.
*
* @param $maxQueryComplexity
*/
public function setMaxQueryComplexity($maxQueryComplexity)
{
$this->checkIfGreaterOrEqualToZero('maxQueryComplexity', $maxQueryComplexity);
$this->maxQueryComplexity = (int) $maxQueryComplexity;
}
public function getMaxQueryComplexity()
{
return $this->maxQueryComplexity;
}
public function setRawVariableValues(array $rawVariableValues = null)
{
$this->rawVariableValues = $rawVariableValues ?: [];
}
public function getRawVariableValues()
{
return $this->rawVariableValues;
}
public function getVisitor(ValidationContext $context)
{
$this->context = $context;
$this->variableDefs = new \ArrayObject();
$this->fieldNodeAndDefs = new \ArrayObject();
$complexity = 0;
$this->variableDefs = new ArrayObject();
$this->fieldNodeAndDefs = new ArrayObject();
$this->complexity = 0;
return $this->invokeIfNeeded(
$context,
[
NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context) {
NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context) : void {
$this->fieldNodeAndDefs = $this->collectFieldASTsAndDefs(
$context,
$context->getParentType(),
@@ -88,23 +70,31 @@ class QueryComplexity extends AbstractQuerySecurity
$this->fieldNodeAndDefs
);
},
NodeKind::VARIABLE_DEFINITION => function ($def) {
NodeKind::VARIABLE_DEFINITION => function ($def) : VisitorOperation {
$this->variableDefs[] = $def;
return Visitor::skipNode();
},
NodeKind::OPERATION_DEFINITION => [
'leave' => function (OperationDefinitionNode $operationDefinition) use ($context, &$complexity) {
'leave' => function (OperationDefinitionNode $operationDefinition) use ($context, &$complexity) : void {
$errors = $context->getErrors();
if (empty($errors)) {
$complexity = $this->fieldComplexity($operationDefinition, $complexity);
if ($complexity > $this->getMaxQueryComplexity()) {
$context->reportError(
new Error($this->maxQueryComplexityErrorMessage($this->getMaxQueryComplexity(), $complexity))
);
}
if (count($errors) > 0) {
return;
}
$this->complexity = $this->fieldComplexity($operationDefinition, $complexity);
if ($this->getQueryComplexity() <= $this->getMaxQueryComplexity()) {
return;
}
$context->reportError(
new Error(self::maxQueryComplexityErrorMessage(
$this->getMaxQueryComplexity(),
$this->getQueryComplexity()
))
);
},
],
]
@@ -124,11 +114,10 @@ class QueryComplexity extends AbstractQuerySecurity
private function nodeComplexity(Node $node, $complexity = 0)
{
switch ($node->kind) {
case NodeKind::FIELD:
/* @var FieldNode $node */
switch (true) {
case $node instanceof FieldNode:
// default values
$args = [];
$args = [];
$complexityFn = FieldDefinition::DEFAULT_COMPLEXITY_FN;
// calculate children complexity if needed
@@ -140,7 +129,7 @@ class QueryComplexity extends AbstractQuerySecurity
}
$astFieldInfo = $this->astFieldInfo($node);
$fieldDef = $astFieldInfo[1];
$fieldDef = $astFieldInfo[1];
if ($fieldDef instanceof FieldDefinition) {
if ($this->directiveExcludesField($node)) {
@@ -154,22 +143,20 @@ class QueryComplexity extends AbstractQuerySecurity
}
}
$complexity += call_user_func_array($complexityFn, [$childrenComplexity, $args]);
$complexity += $complexityFn($childrenComplexity, $args);
break;
case NodeKind::INLINE_FRAGMENT:
/* @var InlineFragmentNode $node */
case $node instanceof InlineFragmentNode:
// node has children?
if (isset($node->selectionSet)) {
$complexity = $this->fieldComplexity($node, $complexity);
}
break;
case NodeKind::FRAGMENT_SPREAD:
/* @var FragmentSpreadNode $node */
case $node instanceof FragmentSpreadNode:
$fragment = $this->getFragment($node);
if (null !== $fragment) {
if ($fragment !== null) {
$complexity = $this->fieldComplexity($fragment, $complexity);
}
break;
@@ -180,11 +167,11 @@ class QueryComplexity extends AbstractQuerySecurity
private function astFieldInfo(FieldNode $field)
{
$fieldName = $this->getFieldName($field);
$fieldName = $this->getFieldName($field);
$astFieldInfo = [null, null];
if (isset($this->fieldNodeAndDefs[$fieldName])) {
foreach ($this->fieldNodeAndDefs[$fieldName] as $astAndDef) {
if ($astAndDef[0] == $field) {
if ($astAndDef[0] === $field) {
$astFieldInfo = $astAndDef;
break;
}
@@ -194,54 +181,120 @@ class QueryComplexity extends AbstractQuerySecurity
return $astFieldInfo;
}
private function directiveExcludesField(FieldNode $node)
{
foreach ($node->directives as $directiveNode) {
if ($directiveNode->name->value === 'deprecated') {
return false;
}
[$errors, $variableValues] = Values::getVariableValues(
$this->context->getSchema(),
$this->variableDefs,
$this->getRawVariableValues()
);
if (count($errors ?? []) > 0) {
throw new Error(implode(
"\n\n",
array_map(
static function ($error) {
return $error->getMessage();
},
$errors
)
));
}
if ($directiveNode->name->value === 'include') {
$directive = Directive::includeDirective();
/** @var bool $directiveArgsIf */
$directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues)['if'];
return ! $directiveArgsIf;
}
if ($directiveNode->name->value === Directive::SKIP_NAME) {
$directive = Directive::skipDirective();
/** @var bool $directiveArgsIf */
$directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues)['if'];
return $directiveArgsIf;
}
}
return false;
}
public function getRawVariableValues()
{
return $this->rawVariableValues;
}
/**
* @param mixed[]|null $rawVariableValues
*/
public function setRawVariableValues(?array $rawVariableValues = null)
{
$this->rawVariableValues = $rawVariableValues ?? [];
}
private function buildFieldArguments(FieldNode $node)
{
$rawVariableValues = $this->getRawVariableValues();
$astFieldInfo = $this->astFieldInfo($node);
$fieldDef = $astFieldInfo[1];
$astFieldInfo = $this->astFieldInfo($node);
$fieldDef = $astFieldInfo[1];
$args = [];
if ($fieldDef instanceof FieldDefinition) {
$variableValues = Values::getVariableValues(
[$errors, $variableValues] = Values::getVariableValues(
$this->context->getSchema(),
$this->variableDefs,
$rawVariableValues
);
if (count($errors ?? []) > 0) {
throw new Error(implode(
"\n\n",
array_map(
static function ($error) {
return $error->getMessage();
},
$errors
)
));
}
$args = Values::getArgumentValues($fieldDef, $node, $variableValues);
}
return $args;
}
private function directiveExcludesField(FieldNode $node) {
foreach ($node->directives as $directiveNode) {
if ($directiveNode->name->value === 'deprecated') {
return false;
}
public function getQueryComplexity()
{
return $this->complexity;
}
$variableValues = Values::getVariableValues(
$this->context->getSchema(),
$this->variableDefs,
$this->getRawVariableValues()
);
public function getMaxQueryComplexity()
{
return $this->maxQueryComplexity;
}
if ($directiveNode->name->value === 'include') {
$directive = Directive::includeDirective();
$directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues);
/**
* 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);
return !$directiveArgs['if'];
} else {
$directive = Directive::skipDirective();
$directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues);
$this->maxQueryComplexity = (int) $maxQueryComplexity;
}
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()
{
return $this->getMaxQueryComplexity() !== static::DISABLED;
return $this->getMaxQueryComplexity() !== self::DISABLED;
}
}
+45 -46
View File
@@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
@@ -10,12 +13,11 @@ 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,53 +25,28 @@ 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(
$context,
[
NodeKind::OPERATION_DEFINITION => [
'leave' => function (OperationDefinitionNode $operationDefinition) use ($context) {
'leave' => function (OperationDefinitionNode $operationDefinition) use ($context) : void {
$maxDepth = $this->fieldDepth($operationDefinition);
if ($maxDepth > $this->getMaxQueryDepth()) {
$context->reportError(
new Error($this->maxQueryDepthErrorMessage($this->getMaxQueryDepth(), $maxDepth))
);
if ($maxDepth <= $this->getMaxQueryDepth()) {
return;
}
$context->reportError(
new Error(self::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) {
@@ -83,11 +60,10 @@ class QueryDepth extends AbstractQuerySecurity
private function nodeDepth(Node $node, $depth = 0, $maxDepth = 0)
{
switch ($node->kind) {
case NodeKind::FIELD:
/* @var FieldNode $node */
switch (true) {
case $node instanceof FieldNode:
// node has children?
if (null !== $node->selectionSet) {
if ($node->selectionSet !== null) {
// update maxDepth if needed
if ($depth > $maxDepth) {
$maxDepth = $depth;
@@ -96,19 +72,17 @@ class QueryDepth extends AbstractQuerySecurity
}
break;
case NodeKind::INLINE_FRAGMENT:
/* @var InlineFragmentNode $node */
case $node instanceof InlineFragmentNode:
// 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 */
case $node instanceof FragmentSpreadNode:
$fragment = $this->getFragment($node);
if (null !== $fragment) {
if ($fragment !== null) {
$maxDepth = $this->fieldDepth($fragment, $depth, $maxDepth);
}
break;
@@ -116,4 +90,29 @@ 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.
*/
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() !== self::DISABLED;
}
}
@@ -1,30 +1,54 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use ArrayObject;
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 InvalidArgumentException;
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[]
* check if equal to 0 no check is done. Must be greater or equal to 0.
*
* @param string $name
* @param int $value
*/
protected function checkIfGreaterOrEqualToZero($name, $value)
{
if ($value < 0) {
throw new InvalidArgumentException(sprintf('$%s argument must be greater or equal to 0.', $name));
}
}
protected function getFragment(FragmentSpreadNode $fragmentSpread)
{
$spreadName = $fragmentSpread->name->value;
$fragments = $this->getFragments();
return $fragments[$spreadName] ?? null;
}
/**
* @return FragmentDefinitionNode[]
*/
protected function getFragments()
{
@@ -32,49 +56,38 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule
}
/**
* check if equal to 0 no check is done. Must be greater or equal to 0.
* @param callable[] $validators
*
* @param $value
* @return callable[]
*/
protected function checkIfGreaterOrEqualToZero($name, $value)
protected function invokeIfNeeded(ValidationContext $context, array $validators)
{
if ($value < 0) {
throw new \InvalidArgumentException(sprintf('$%s argument must be greater or equal to 0.', $name));
// is disabled?
if (! $this->isEnabled()) {
return [];
}
$this->gatherFragmentDefinition($context);
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) {
$this->fragments[$node->name->value] = $node;
if (! ($node instanceof FragmentDefinitionNode)) {
continue;
}
$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;
}
protected function invokeIfNeeded(ValidationContext $context, array $validators)
{
// is disabled?
if (!$this->isEnabled()) {
return [];
}
$this->gatherFragmentDefinition($context);
return $validators;
}
/**
* Given a selectionSet, adds all of the fields in that selection to
* the passed in map of fields, and returns it at the end.
@@ -85,29 +98,29 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule
*
* @see \GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged
*
* @param ValidationContext $context
* @param Type|null $parentType
* @param SelectionSetNode $selectionSet
* @param \ArrayObject $visitedFragmentNames
* @param \ArrayObject $astAndDefs
* @param Type|null $parentType
*
* @return \ArrayObject
* @return ArrayObject
*/
protected function collectFieldASTsAndDefs(ValidationContext $context, $parentType, SelectionSetNode $selectionSet, \ArrayObject $visitedFragmentNames = null, \ArrayObject $astAndDefs = null)
{
$_visitedFragmentNames = $visitedFragmentNames ?: new \ArrayObject();
$_astAndDefs = $astAndDefs ?: new \ArrayObject();
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) {
case NodeKind::FIELD:
/* @var FieldNode $selection */
switch (true) {
case $selection instanceof FieldNode:
$fieldName = $selection->name->value;
$fieldDef = null;
$fieldDef = null;
if ($parentType && method_exists($parentType, 'getFields')) {
$tmp = $parentType->getFields();
$schemaMetaFieldDef = Introspection::schemaMetaFieldDef();
$typeMetaFieldDef = Introspection::typeMetaFieldDef();
$tmp = $parentType->getFields();
$schemaMetaFieldDef = Introspection::schemaMetaFieldDef();
$typeMetaFieldDef = Introspection::typeMetaFieldDef();
$typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef();
if ($fieldName === $schemaMetaFieldDef->name && $context->getSchema()->getQueryType() === $parentType) {
@@ -121,14 +134,13 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule
}
}
$responseName = $this->getFieldName($selection);
if (!isset($_astAndDefs[$responseName])) {
$_astAndDefs[$responseName] = new \ArrayObject();
if (! isset($_astAndDefs[$responseName])) {
$_astAndDefs[$responseName] = new ArrayObject();
}
// create field context
$_astAndDefs[$responseName][] = [$selection, $fieldDef];
break;
case NodeKind::INLINE_FRAGMENT:
/* @var InlineFragmentNode $selection */
case $selection instanceof InlineFragmentNode:
$_astAndDefs = $this->collectFieldASTsAndDefs(
$context,
TypeInfo::typeFromAST($context->getSchema(), $selection->typeCondition),
@@ -137,13 +149,12 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule
$_astAndDefs
);
break;
case NodeKind::FRAGMENT_SPREAD:
/* @var FragmentSpreadNode $selection */
case $selection instanceof FragmentSpreadNode:
$fragName = $selection->name->value;
if (empty($_visitedFragmentNames[$fragName])) {
if (! ($_visitedFragmentNames[$fragName] ?? false)) {
$_visitedFragmentNames[$fragName] = true;
$fragment = $context->getFragment($fragName);
$fragment = $context->getFragment($fragName);
if ($fragment) {
$_astAndDefs = $this->collectFieldASTsAndDefs(
@@ -165,10 +176,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');
+30 -24
View File
@@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
@@ -6,40 +9,43 @@ 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 => static function (FieldNode $node) use ($context) : void {
$type = $context->getType();
if ($type) {
if (Type::isLeafType(Type::getNamedType($type))) {
if ($node->selectionSet) {
$context->reportError(new Error(
self::noSubselectionAllowedMessage($node->name->value, $type),
[$node->selectionSet]
));
}
} else if (!$node->selectionSet) {
if (! $type) {
return;
}
if (Type::isLeafType(Type::getNamedType($type))) {
if ($node->selectionSet) {
$context->reportError(new Error(
self::requiredSubselectionMessage($node->name->value, $type),
[$node]
self::noSubselectionAllowedMessage($node->name->value, $type),
[$node->selectionSet]
));
}
} elseif (! $node->selectionSet) {
$context->reportError(new Error(
self::requiredSubselectionMessage($node->name->value, $type),
[$node]
));
}
}
},
];
}
public static function noSubselectionAllowedMessage($field, $type)
{
return sprintf('Field "%s" of type "%s" must not have a sub selection.', $field, $type);
}
public static function requiredSubselectionMessage($field, $type)
{
return sprintf('Field "%s" of type "%s" must have a sub selection.', $field, $type);
}
}
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\Visitor;
use GraphQL\Language\VisitorOperation;
use GraphQL\Validator\ValidationContext;
use function array_splice;
use function count;
use function sprintf;
class SingleFieldSubscription extends ValidationRule
{
/**
* @return array<string, callable>
*/
public function getVisitor(ValidationContext $context) : array
{
return [
NodeKind::OPERATION_DEFINITION => static function (OperationDefinitionNode $node) use ($context) : VisitorOperation {
if ($node->operation === 'subscription') {
$selections = $node->selectionSet->selections;
if (count($selections) !== 1) {
if ($selections instanceof NodeList) {
$offendingSelections = $selections->splice(1, count($selections));
} else {
$offendingSelections = array_splice($selections, 1);
}
$context->reportError(new Error(
self::multipleFieldsInOperation($node->name->value ?? null),
$offendingSelections
));
}
}
return Visitor::skipNode();
},
];
}
public static function multipleFieldsInOperation(?string $operationName) : string
{
if ($operationName === null) {
return sprintf('Anonymous Subscription must select only one top level field.');
}
return sprintf('Subscription "%s" must select only one top level field.', $operationName);
}
}
@@ -1,36 +1,49 @@
<?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\Language\VisitorOperation;
use GraphQL\Validator\ASTValidationContext;
use GraphQL\Validator\SDLValidationContext;
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 getSDLVisitor(SDLValidationContext $context)
{
return $this->getASTVisitor($context);
}
public function getVisitor(ValidationContext $context)
{
return $this->getASTVisitor($context);
}
public function getASTVisitor(ASTValidationContext $context)
{
$this->knownArgNames = [];
return [
NodeKind::FIELD => function () {
$this->knownArgNames = [];;
},
NodeKind::DIRECTIVE => function () {
NodeKind::FIELD => function () : void {
$this->knownArgNames = [];
},
NodeKind::ARGUMENT => function (ArgumentNode $node) use ($context) {
NodeKind::DIRECTIVE => function () : void {
$this->knownArgNames = [];
},
NodeKind::ARGUMENT => function (ArgumentNode $node) use ($context) : VisitorOperation {
$argName = $node->name->value;
if (!empty($this->knownArgNames[$argName])) {
if ($this->knownArgNames[$argName] ?? false) {
$context->reportError(new Error(
self::duplicateArgMessage($argName),
[$this->knownArgNames[$argName], $node->name]
@@ -38,8 +51,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);
}
}
@@ -1,38 +1,89 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\Node;
use GraphQL\Type\Definition\Directive;
use GraphQL\Validator\ASTValidationContext;
use GraphQL\Validator\SDLValidationContext;
use GraphQL\Validator\ValidationContext;
use function sprintf;
class UniqueDirectivesPerLocation extends AbstractValidationRule
/**
* Unique directive names per location
*
* A GraphQL document is only valid if all non-repeatable directives at
* a given location are uniquely named.
*/
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 $this->getASTVisitor($context);
}
public function getSDLVisitor(SDLValidationContext $context)
{
return $this->getASTVisitor($context);
}
public function getASTVisitor(ASTValidationContext $context)
{
$uniqueDirectiveMap = [];
$schema = $context->getSchema();
$definedDirectives = $schema !== null
? $schema->getDirectives()
: Directive::getInternalDirectives();
foreach ($definedDirectives as $directive) {
$uniqueDirectiveMap[$directive->name] = ! $directive->isRepeatable;
}
$astDefinitions = $context->getDocument()->definitions;
foreach ($astDefinitions as $definition) {
if (! ($definition instanceof DirectiveDefinitionNode)) {
continue;
}
$uniqueDirectiveMap[$definition->name->value] = $definition->repeatable;
}
return [
'enter' => function(Node $node) use ($context) {
if (isset($node->directives)) {
$knownDirectives = [];
foreach ($node->directives as $directive) {
/** @var DirectiveNode $directive */
$directiveName = $directive->name->value;
if (isset($knownDirectives[$directiveName])) {
$context->reportError(new Error(
self::duplicateDirectiveMessage($directiveName),
[$knownDirectives[$directiveName], $directive]
));
} else {
$knownDirectives[$directiveName] = $directive;
}
'enter' => static function (Node $node) use ($uniqueDirectiveMap, $context) : void {
if (! isset($node->directives)) {
return;
}
$knownDirectives = [];
/** @var DirectiveNode $directive */
foreach ($node->directives as $directive) {
$directiveName = $directive->name->value;
if (! isset($uniqueDirectiveMap[$directiveName])) {
continue;
}
if (isset($knownDirectives[$directiveName])) {
$context->reportError(new Error(
self::duplicateDirectiveMessage($directiveName),
[$knownDirectives[$directiveName], $directive]
));
} else {
$knownDirectives[$directiveName] = $directive;
}
}
}
},
];
}
public static function duplicateDirectiveMessage($directiveName)
{
return sprintf('The directive "%s" can only be used once at this location.', $directiveName);
}
}
@@ -1,21 +1,21 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NameNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Visitor;
use GraphQL\Language\VisitorOperation;
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)
@@ -23,21 +23,27 @@ class UniqueFragmentNames extends AbstractValidationRule
$this->knownFragmentNames = [];
return [
NodeKind::OPERATION_DEFINITION => function () {
NodeKind::OPERATION_DEFINITION => static function () : VisitorOperation {
return Visitor::skipNode();
},
NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) {
NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) : VisitorOperation {
$fragmentName = $node->name->value;
if (!empty($this->knownFragmentNames[$fragmentName])) {
if (! isset($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);
}
}
@@ -1,51 +1,73 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NameNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\ObjectFieldNode;
use GraphQL\Language\Visitor;
use GraphQL\Language\VisitorOperation;
use GraphQL\Validator\ASTValidationContext;
use GraphQL\Validator\SDLValidationContext;
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 array<string, NameNode> */
public $knownNames;
/** @var array<array<string, NameNode>> */
public $knownNameStack;
public function getVisitor(ValidationContext $context)
{
$this->knownNames = [];
return $this->getASTVisitor($context);
}
public function getSDLVisitor(SDLValidationContext $context)
{
return $this->getASTVisitor($context);
}
public function getASTVisitor(ASTValidationContext $context)
{
$this->knownNames = [];
$this->knownNameStack = [];
return [
NodeKind::OBJECT => [
'enter' => function() {
NodeKind::OBJECT => [
'enter' => function () : void {
$this->knownNameStack[] = $this->knownNames;
$this->knownNames = [];
$this->knownNames = [];
},
'leave' => function() {
'leave' => function () : void {
$this->knownNames = array_pop($this->knownNameStack);
}
},
],
NodeKind::OBJECT_FIELD => function(ObjectFieldNode $node) use ($context) {
NodeKind::OBJECT_FIELD => function (ObjectFieldNode $node) use ($context) : VisitorOperation {
$fieldName = $node->name->value;
if (!empty($this->knownNames[$fieldName])) {
if (isset($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);
}
}
@@ -1,20 +1,21 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NameNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\Visitor;
use GraphQL\Language\VisitorOperation;
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)
@@ -22,24 +23,30 @@ class UniqueOperationNames extends AbstractValidationRule
$this->knownOperationNames = [];
return [
NodeKind::OPERATION_DEFINITION => function(OperationDefinitionNode $node) use ($context) {
NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) use ($context) : VisitorOperation {
$operationName = $node->name;
if ($operationName) {
if (!empty($this->knownOperationNames[$operationName->value])) {
if ($operationName !== null) {
if (! isset($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 => static function () : VisitorOperation {
return Visitor::skipNode();
}
},
];
}
public static function duplicateOperationNameMessage($operationName)
{
return sprintf('There can be only one operation named "%s".', $operationName);
}
}
@@ -1,19 +1,19 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\Node;
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)
@@ -21,20 +21,25 @@ class UniqueVariableNames extends AbstractValidationRule
$this->knownVariableNames = [];
return [
NodeKind::OPERATION_DEFINITION => function() {
NodeKind::OPERATION_DEFINITION => function () : void {
$this->knownVariableNames = [];
},
NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $node) use ($context) {
NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) use ($context) : void {
$variableName = $node->variable->name->value;
if (!empty($this->knownVariableNames[$variableName])) {
if (! isset($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);
}
}
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Validator\SDLValidationContext;
use GraphQL\Validator\ValidationContext;
use function class_alias;
abstract class ValidationRule
{
/** @var string */
protected $name;
public function getName()
{
return $this->name === '' || $this->name === null ? static::class : $this->name;
}
public function __invoke(ValidationContext $context)
{
return $this->getVisitor($context);
}
/**
* Returns structure suitable for GraphQL\Language\Visitor
*
* @see \GraphQL\Language\Visitor
*
* @return mixed[]
*/
public function getVisitor(ValidationContext $context)
{
return [];
}
/**
* Returns structure suitable for GraphQL\Language\Visitor
*
* @see \GraphQL\Language\Visitor
*
* @return mixed[]
*/
public function getSDLVisitor(SDLValidationContext $context)
{
return [];
}
}
class_alias(ValidationRule::class, 'GraphQL\Validator\Rules\AbstractValidationRule');
@@ -0,0 +1,288 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\BooleanValueNode;
use GraphQL\Language\AST\EnumValueNode;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FloatValueNode;
use GraphQL\Language\AST\IntValueNode;
use GraphQL\Language\AST\ListValueNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NullValueNode;
use GraphQL\Language\AST\ObjectFieldNode;
use GraphQL\Language\AST\ObjectValueNode;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Language\AST\ValueNode;
use GraphQL\Language\AST\VariableNode;
use GraphQL\Language\Printer;
use GraphQL\Language\Visitor;
use GraphQL\Language\VisitorOperation;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\EnumValueDefinition;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext;
use Throwable;
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
*
* A GraphQL document is only valid if all value literals are of the type
* expected at their position.
*/
class ValuesOfCorrectType extends ValidationRule
{
public function getVisitor(ValidationContext $context)
{
$fieldName = '';
return [
NodeKind::FIELD => [
'enter' => static function (FieldNode $node) use (&$fieldName) : void {
$fieldName = $node->name->value;
},
],
NodeKind::NULL => static function (NullValueNode $node) use ($context, &$fieldName) : void {
$type = $context->getInputType();
if (! ($type instanceof NonNull)) {
return;
}
$context->reportError(
new Error(
self::getBadValueMessage((string) $type, Printer::doPrint($node), null, $context, $fieldName),
$node
)
);
},
NodeKind::LST => function (ListValueNode $node) use ($context, &$fieldName) : ?VisitorOperation {
// 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) {
$this->isValidScalar($context, $node, $fieldName);
return Visitor::skipNode();
}
return null;
},
NodeKind::OBJECT => function (ObjectValueNode $node) use ($context, &$fieldName) {
// 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) {
$this->isValidScalar($context, $node, $fieldName);
return Visitor::skipNode();
}
unset($fieldName);
// Ensure every required field exists.
$inputFields = $type->getFields();
$nodeFields = iterator_to_array($node->fields);
$fieldNodeMap = array_combine(
array_map(
static function ($field) : string {
return $field->name->value;
},
$nodeFields
),
array_values($nodeFields)
);
foreach ($inputFields as $fieldName => $fieldDef) {
$fieldType = $fieldDef->getType();
if (isset($fieldNodeMap[$fieldName]) || ! $fieldDef->isRequired()) {
continue;
}
$context->reportError(
new Error(
self::requiredFieldMessage($type->name, $fieldName, (string) $fieldType),
$node
)
);
}
},
NodeKind::OBJECT_FIELD => static function (ObjectFieldNode $node) use ($context) : void {
$parentType = Type::getNamedType($context->getParentInputType());
/** @var ScalarType|EnumType|InputObjectType|ListOfType|NonNull $fieldType */
$fieldType = $context->getInputType();
if ($fieldType || ! ($parentType instanceof InputObjectType)) {
return;
}
$suggestions = Utils::suggestionList(
$node->name->value,
array_keys($parentType->getFields())
);
$didYouMean = $suggestions
? 'Did you mean ' . Utils::orList($suggestions) . '?'
: null;
$context->reportError(
new Error(
self::unknownFieldMessage($parentType->name, $node->name->value, $didYouMean),
$node
)
);
},
NodeKind::ENUM => function (EnumValueNode $node) use ($context, &$fieldName) : void {
$type = Type::getNamedType($context->getInputType());
if (! $type instanceof EnumType) {
$this->isValidScalar($context, $node, $fieldName);
} elseif (! $type->getValue($node->value)) {
$context->reportError(
new Error(
self::getBadValueMessage(
$type->name,
Printer::doPrint($node),
$this->enumTypeSuggestion($type, $node),
$context,
$fieldName
),
$node
)
);
}
},
NodeKind::INT => function (IntValueNode $node) use ($context, &$fieldName) : void {
$this->isValidScalar($context, $node, $fieldName);
},
NodeKind::FLOAT => function (FloatValueNode $node) use ($context, &$fieldName) : void {
$this->isValidScalar($context, $node, $fieldName);
},
NodeKind::STRING => function (StringValueNode $node) use ($context, &$fieldName) : void {
$this->isValidScalar($context, $node, $fieldName);
},
NodeKind::BOOLEAN => function (BooleanValueNode $node) use ($context, &$fieldName) : void {
$this->isValidScalar($context, $node, $fieldName);
},
];
}
public static function badValueMessage($typeName, $valueName, $message = null)
{
return sprintf('Expected type %s, found %s', $typeName, $valueName) .
($message ? "; ${message}" : '.');
}
/**
* @param VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode $node
*/
private function isValidScalar(ValidationContext $context, ValueNode $node, $fieldName)
{
// Report any error at the full type expected by the location.
/** @var ScalarType|EnumType|InputObjectType|ListOfType|NonNull $locationType */
$locationType = $context->getInputType();
if (! $locationType) {
return;
}
$type = Type::getNamedType($locationType);
if (! $type instanceof ScalarType) {
$context->reportError(
new Error(
self::getBadValueMessage(
(string) $locationType,
Printer::doPrint($node),
$this->enumTypeSuggestion($type, $node),
$context,
$fieldName
),
$node
)
);
return;
}
// Scalars determine if a literal value is valid via parseLiteral() which
// may throw to indicate failure.
try {
$type->parseLiteral($node);
} catch (Throwable $error) {
// Ensure a reference to the original error is maintained.
$context->reportError(
new Error(
self::getBadValueMessage(
(string) $locationType,
Printer::doPrint($node),
$error->getMessage(),
$context,
$fieldName
),
$node,
null,
[],
null,
$error
)
);
}
}
/**
* @param VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode $node
*/
private function enumTypeSuggestion($type, ValueNode $node)
{
if ($type instanceof EnumType) {
$suggestions = Utils::suggestionList(
Printer::doPrint($node),
array_map(
static function (EnumValueDefinition $value) : string {
return $value->name;
},
$type->getValues()
)
);
return $suggestions ? 'Did you mean the enum value ' . Utils::orList($suggestions) . '?' : null;
}
}
public static function badArgumentValueMessage($typeName, $valueName, $fieldName, $argName, $message = null)
{
return sprintf('Field "%s" argument "%s" requires type %s, found %s', $fieldName, $argName, $typeName, $valueName) .
($message ? sprintf('; %s', $message) : '.');
}
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) : '.');
}
private static function getBadValueMessage($typeName, $valueName, $message = null, $context = null, $fieldName = null)
{
if ($context) {
$arg = $context->getArgument();
if ($arg) {
return self::badArgumentValueMessage($typeName, $valueName, $fieldName, $arg->name, $message);
}
}
return self::badValueMessage($typeName, $valueName, $message);
}
}
@@ -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 => static function (VariableDefinitionNode $node) use ($context) : void {
$type = TypeInfo::typeFromAST($context->getSchema(), $node->type);
// If the variable type is not an input type, return an error.
if ($type && !Type::isInputType($type)) {
$variableName = $node->variable->name->value;
$context->reportError(new Error(
self::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)),
[ $node->type ]
));
if (! $type || Type::isInputType($type)) {
return;
}
}
$variableName = $node->variable->name->value;
$context->reportError(new Error(
self::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)),
[$node->type]
));
},
];
}
public static function nonInputTypeOnVarMessage($variableName, $typeName)
{
return sprintf('Variable "$%s" cannot be non-input type "%s".', $variableName, $typeName);
}
}
@@ -1,92 +1,116 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NullValueNode;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\ValueNode;
use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use GraphQL\Utils\TypeComparators;
use GraphQL\Utils\TypeInfo;
use GraphQL\Utils\Utils;
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\".";
}
/**
* A map from variable names to their definition nodes.
*
* @var VariableDefinitionNode[]
*/
public $varDefMap;
public function getVisitor(ValidationContext $context)
{
return [
NodeKind::OPERATION_DEFINITION => [
'enter' => function () {
'enter' => function () : void {
$this->varDefMap = [];
},
'leave' => function(OperationDefinitionNode $operation) use ($context) {
'leave' => function (OperationDefinitionNode $operation) use ($context) : void {
$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;
$node = $usage['node'];
$type = $usage['type'];
$defaultValue = $usage['defaultValue'];
$varName = $node->name->value;
$varDef = $this->varDefMap[$varName] ?? null;
if ($varDef && $type) {
// A var type is allowed if it is the same or more strict (e.g. is
// a subtype of) than the expected type. It can be more strict if
// the variable type is non-null when the expected type is nullable.
// If both are list types, the variable item type can be more strict
// than the expected item type (contravariant).
$schema = $context->getSchema();
$varType = TypeInfo::typeFromAST($schema, $varDef->type);
if ($varType && !TypeComparators::isTypeSubTypeOf($schema, $this->effectiveType($varType, $varDef), $type)) {
$context->reportError(new Error(
self::badVarPosMessage($varName, $varType, $type),
[$varDef, $node]
));
}
if ($varDef === null || $type === null) {
continue;
}
// A var type is allowed if it is the same or more strict (e.g. is
// a subtype of) than the expected type. It can be more strict if
// the variable type is non-null when the expected type is nullable.
// If both are list types, the variable item type can be more strict
// than the expected item type (contravariant).
$schema = $context->getSchema();
$varType = TypeInfo::typeFromAST($schema, $varDef->type);
if (! $varType || $this->allowedVariableUsage($schema, $varType, $varDef->defaultValue, $type, $defaultValue)) {
continue;
}
$context->reportError(new Error(
self::badVarPosMessage($varName, $varType, $type),
[$varDef, $node]
));
}
}
},
],
NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $varDefNode) {
NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $varDefNode) : void {
$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 varTypeAllowedForType($varType, $expectedType)
/**
* 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)
{
if ($expectedType instanceof NonNull) {
if ($varType instanceof NonNull) {
return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType->getWrappedType());
return sprintf(
'Variable "$%s" of type "%s" used in position expecting type "%s".',
$varName,
$varType,
$expectedType
);
}
/**
* Returns true if the variable is allowed in the location it was found,
* which includes considering if default values exist for either the variable
* or the location at which it is located.
*
* @param ValueNode|null $varDefaultValue
* @param mixed $locationDefaultValue
*/
private function allowedVariableUsage(Schema $schema, Type $varType, $varDefaultValue, Type $locationType, $locationDefaultValue) : bool
{
if ($locationType instanceof NonNull && ! $varType instanceof NonNull) {
$hasNonNullVariableDefaultValue = $varDefaultValue && ! $varDefaultValue instanceof NullValueNode;
$hasLocationDefaultValue = ! Utils::isInvalid($locationDefaultValue);
if (! $hasNonNullVariableDefaultValue && ! $hasLocationDefaultValue) {
return false;
}
return false;
}
if ($varType instanceof NonNull) {
return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType);
}
if ($varType instanceof ListOfType && $expectedType instanceof ListOfType) {
return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType->getWrappedType());
}
return $varType === $expectedType;
}
$nullableLocationType = $locationType->getWrappedType();
// 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);
}
return TypeComparators::isTypeSubTypeOf($schema, $varType, $nullableLocationType);
}
return TypeComparators::isTypeSubTypeOf($schema, $varType, $locationType);
}
}
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator;
class SDLValidationContext extends ASTValidationContext
{
}
+161 -185
View File
@@ -1,294 +1,270 @@
<?php
declare(strict_types=1);
namespace GraphQL\Validator;
use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\HasSelectionSet;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\SelectionSetNode;
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\Language\AST\Node;
use GraphQL\Type\Definition\CompositeType;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\OutputType;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use GraphQL\Utils\TypeInfo;
use SplObjectStorage;
use function array_merge;
use function array_pop;
use function count;
/**
* An instance of this class is passed as the "this" context to all validators,
* allowing access to commonly useful contextual information from within a
* validation rule.
*/
class ValidationContext
class ValidationContext extends ASTValidationContext
{
/**
* @var Schema
*/
private $schema;
/**
* @var DocumentNode
*/
private $ast;
/**
* @var TypeInfo
*/
/** @var TypeInfo */
private $typeInfo;
/**
* @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;
$this->typeInfo = $typeInfo;
$this->errors = [];
$this->fragmentSpreads = new SplObjectStorage();
parent::__construct($ast, $schema);
$this->typeInfo = $typeInfo;
$this->fragmentSpreads = new SplObjectStorage();
$this->recursivelyReferencedFragments = new SplObjectStorage();
$this->variableUsages = new SplObjectStorage();
$this->recursiveVariableUsages = new SplObjectStorage();
$this->variableUsages = new SplObjectStorage();
$this->recursiveVariableUsages = new SplObjectStorage();
}
/**
* @param Error $error
* @return mixed[][] List of ['node' => VariableNode, 'type' => ?InputObjectType]
*/
function reportError(Error $error)
public function getRecursiveVariableUsages(OperationDefinitionNode $operation)
{
$this->errors[] = $error;
$usages = $this->recursiveVariableUsages[$operation] ?? null;
if ($usages === null) {
$usages = $this->getVariableUsages($operation);
$fragments = $this->getRecursivelyReferencedFragments($operation);
$allUsages = [$usages];
foreach ($fragments as $fragment) {
$allUsages[] = $this->getVariableUsages($fragment);
}
$usages = array_merge(...$allUsages);
$this->recursiveVariableUsages[$operation] = $usages;
}
return $usages;
}
/**
* @return Error[]
* @return mixed[][] List of ['node' => VariableNode, 'type' => ?InputObjectType]
*/
function getErrors()
private function getVariableUsages(HasSelectionSet $node)
{
return $this->errors;
$usages = $this->variableUsages[$node] ?? null;
if ($usages === null) {
$newUsages = [];
$typeInfo = new TypeInfo($this->schema);
Visitor::visit(
$node,
Visitor::visitWithTypeInfo(
$typeInfo,
[
NodeKind::VARIABLE_DEFINITION => static function () : bool {
return false;
},
NodeKind::VARIABLE => static function (VariableNode $variable) use (
&$newUsages,
$typeInfo
) : void {
$newUsages[] = [
'node' => $variable,
'type' => $typeInfo->getInputType(),
'defaultValue' => $typeInfo->getDefaultValue(),
];
},
]
)
);
$usages = $newUsages;
$this->variableUsages[$node] = $usages;
}
return $usages;
}
/**
* @return Schema
* @return FragmentDefinitionNode[]
*/
function getSchema()
public function getRecursivelyReferencedFragments(OperationDefinitionNode $operation)
{
return $this->schema;
}
$fragments = $this->recursivelyReferencedFragments[$operation] ?? null;
/**
* @return DocumentNode
*/
function getDocument()
{
return $this->ast;
}
if ($fragments === null) {
$fragments = [];
$collectedNames = [];
$nodesToVisit = [$operation];
while (count($nodesToVisit) > 0) {
$node = array_pop($nodesToVisit);
$spreads = $this->getFragmentSpreads($node);
foreach ($spreads as $spread) {
$fragName = $spread->name->value;
/**
* @param $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;
if ($collectedNames[$fragName] ?? false) {
continue;
}
$collectedNames[$fragName] = true;
$fragment = $this->getFragment($fragName);
if (! $fragment) {
continue;
}
$fragments[] = $fragment;
$nodesToVisit[] = $fragment;
}
}
$this->fragments = $fragments;
$this->recursivelyReferencedFragments[$operation] = $fragments;
}
return isset($fragments[$name]) ? $fragments[$name] : null;
return $fragments;
}
/**
* @param HasSelectionSet $node
* @param OperationDefinitionNode|FragmentDefinitionNode $node
*
* @return FragmentSpreadNode[]
*/
function getFragmentSpreads(HasSelectionSet $node)
public function getFragmentSpreads(HasSelectionSet $node) : array
{
$spreads = isset($this->fragmentSpreads[$node]) ? $this->fragmentSpreads[$node] : null;
if (!$spreads) {
$spreads = $this->fragmentSpreads[$node] ?? null;
if ($spreads === null) {
$spreads = [];
/** @var SelectionSetNode[] $setsToVisit */
$setsToVisit = [$node->selectionSet];
while (!empty($setsToVisit)) {
while (count($setsToVisit) > 0) {
$set = array_pop($setsToVisit);
for ($i = 0; $i < count($set->selections); $i++) {
for ($i = 0, $selectionCount = count($set->selections); $i < $selectionCount; $i++) {
$selection = $set->selections[$i];
if ($selection->kind === NodeKind::FRAGMENT_SPREAD) {
if ($selection instanceof FragmentSpreadNode) {
$spreads[] = $selection;
} else if ($selection->selectionSet) {
$setsToVisit[] = $selection->selectionSet;
} elseif ($selection instanceof FieldNode || $selection instanceof InlineFragmentNode) {
if ($selection->selectionSet) {
$setsToVisit[] = $selection->selectionSet;
}
} else {
throw InvariantViolation::shouldNotHappen();
}
}
}
$this->fragmentSpreads[$node] = $spreads;
}
return $spreads;
}
/**
* @param OperationDefinitionNode $operation
* @return FragmentDefinitionNode[]
*/
function getRecursivelyReferencedFragments(OperationDefinitionNode $operation)
{
$fragments = isset($this->recursivelyReferencedFragments[$operation]) ? $this->recursivelyReferencedFragments[$operation] : null;
if (!$fragments) {
$fragments = [];
$collectedNames = [];
$nodesToVisit = [$operation];
while (!empty($nodesToVisit)) {
$node = array_pop($nodesToVisit);
$spreads = $this->getFragmentSpreads($node);
for ($i = 0; $i < count($spreads); $i++) {
$fragName = $spreads[$i]->name->value;
if (empty($collectedNames[$fragName])) {
$collectedNames[$fragName] = true;
$fragment = $this->getFragment($fragName);
if ($fragment) {
$fragments[] = $fragment;
$nodesToVisit[] = $fragment;
}
}
}
}
$this->recursivelyReferencedFragments[$operation] = $fragments;
}
return $fragments;
}
/**
* @param HasSelectionSet $node
* @return array List of ['node' => VariableNode, 'type' => ?InputObjectType]
*/
function getVariableUsages(HasSelectionSet $node)
{
$usages = isset($this->variableUsages[$node]) ? $this->variableUsages[$node] : null;
if (!$usages) {
$newUsages = [];
$typeInfo = new TypeInfo($this->schema);
Visitor::visit($node, Visitor::visitWithTypeInfo($typeInfo, [
NodeKind::VARIABLE_DEFINITION => function () {
return false;
},
NodeKind::VARIABLE => function (VariableNode $variable) use (&$newUsages, $typeInfo) {
$newUsages[] = ['node' => $variable, 'type' => $typeInfo->getInputType()];
}
]));
$usages = $newUsages;
$this->variableUsages[$node] = $usages;
}
return $usages;
}
/**
* @param OperationDefinitionNode $operation
* @return array List of ['node' => VariableNode, 'type' => ?InputObjectType]
*/
function getRecursiveVariableUsages(OperationDefinitionNode $operation)
{
$usages = isset($this->recursiveVariableUsages[$operation]) ? $this->recursiveVariableUsages[$operation] : null;
if (!$usages) {
$usages = $this->getVariableUsages($operation);
$fragments = $this->getRecursivelyReferencedFragments($operation);
$tmp = [$usages];
for ($i = 0; $i < count($fragments); $i++) {
$tmp[] = $this->getVariableUsages($fragments[$i]);
}
$usages = call_user_func_array('array_merge', $tmp);
$this->recursiveVariableUsages[$operation] = $usages;
}
return $usages;
}
/**
* Returns OutputType
* @param string $name
*
* @return Type
* @return FragmentDefinitionNode|null
*/
function getType()
public function getFragment($name)
{
$fragments = $this->fragments;
if (! $fragments) {
$fragments = [];
foreach ($this->getDocument()->definitions as $statement) {
if (! ($statement instanceof FragmentDefinitionNode)) {
continue;
}
$fragments[$statement->name->value] = $statement;
}
$this->fragments = $fragments;
}
return $fragments[$name] ?? null;
}
public function getType() : ?OutputType
{
return $this->typeInfo->getType();
}
/**
* @return CompositeType
* @return (CompositeType & Type) | null
*/
function getParentType()
public function getParentType() : ?CompositeType
{
return $this->typeInfo->getParentType();
}
/**
* @return InputType
* @return (Type & InputType) | null
*/
function getInputType()
public function getInputType() : ?InputType
{
return $this->typeInfo->getInputType();
}
/**
* @return (Type&InputType)|null
*/
public function getParentInputType() : ?InputType
{
return $this->typeInfo->getParentInputType();
}
/**
* @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();
}