diff --git a/CHANGELOG.md b/CHANGELOG.md index 100a1db..3a6599e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## dev-master - Spec compliance: error extensions are displayed under `extensions` key +- `AbstractValidationRule` renamed to `ValidationRule` (NS `GraphQL\Validator\Rules`) +- `AbstractQuerySecurity` renamed to `QuerySecurityRule` (NS `GraphQL\Validator\Rules`) #### v0.12.5 - Execution performance optimization for lists diff --git a/docs/reference.md b/docs/reference.md index e7bba29..cbe890b 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -119,7 +119,7 @@ static function getStandardTypes() * Returns standard validation rules implementing GraphQL spec * * @api - * @return AbstractValidationRule[] + * @return ValidationRule[] */ static function getStandardValidationRules() ``` @@ -1241,7 +1241,7 @@ an empty array if no errors were encountered and the document is valid. A list of specific validation rules may be provided. If not provided, the default list of rules defined by the GraphQL specification will be used. -Each validation rule is an instance of GraphQL\Validator\Rules\AbstractValidationRule +Each validation rule is an instance of GraphQL\Validator\Rules\ValidationRule which returns a visitor (see the [GraphQL\Language\Visitor API](reference.md#graphqllanguagevisitor)). Visitor methods are expected to return an instance of [GraphQL\Error\Error](reference.md#graphqlerrorerror), @@ -1258,7 +1258,7 @@ will be created from the provided schema. * @api * @param Schema $schema * @param DocumentNode $ast - * @param AbstractValidationRule[]|null $rules + * @param ValidationRule[]|null $rules * @param TypeInfo|null $typeInfo * @return Error[] */ @@ -1275,7 +1275,7 @@ static function validate( * Returns all global validation rules. * * @api - * @return AbstractValidationRule[] + * @return ValidationRule[] */ static function allRules() ``` @@ -1289,7 +1289,7 @@ static function allRules() * * @api * @param string $name - * @return AbstractValidationRule + * @return ValidationRule */ static function getRule($name) ``` @@ -1299,9 +1299,9 @@ static function getRule($name) * Add rule to list of global validation rules * * @api - * @param AbstractValidationRule $rule + * @param ValidationRule $rule */ -static function addRule(GraphQL\Validator\Rules\AbstractValidationRule $rule) +static function addRule(GraphQL\Validator\Rules\ValidationRule $rule) ``` # GraphQL\Error\Error Describes an Error found during the parse, validate, or diff --git a/src/Error/Error.php b/src/Error/Error.php index 02c26ec..46f1dbe 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -8,9 +8,9 @@ use GraphQL\Language\AST\Node; use GraphQL\Language\Source; use GraphQL\Language\SourceLocation; use GraphQL\Utils\Utils; +use Traversable; use function array_filter; use function array_map; -use function array_merge; use function is_array; use function iterator_to_array; @@ -81,12 +81,12 @@ class Error extends \Exception implements \JsonSerializable, ClientAware protected $extensions; /** - * @param string $message - * @param Node[]|null $nodes - * @param mixed[]|null $positions - * @param mixed[]|null $path - * @param \Throwable $previous - * @param mixed[] $extensions + * @param string $message + * @param Node|Node[]|Traversable|null $nodes + * @param mixed[]|null $positions + * @param mixed[]|null $path + * @param \Throwable $previous + * @param mixed[] $extensions */ public function __construct( $message, diff --git a/src/GraphQL.php b/src/GraphQL.php index ce863d6..d5c6237 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -13,7 +13,7 @@ use GraphQL\Executor\Promise\PromiseAdapter; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\Type; use GraphQL\Validator\DocumentValidator; -use GraphQL\Validator\Rules\AbstractValidationRule; +use GraphQL\Validator\Rules\ValidationRule; use GraphQL\Validator\Rules\QueryComplexity; /** @@ -272,7 +272,7 @@ class GraphQL * Returns standard validation rules implementing GraphQL spec * * @api - * @return AbstractValidationRule[] + * @return ValidationRule[] */ public static function getStandardValidationRules() { diff --git a/src/Language/AST/FieldNode.php b/src/Language/AST/FieldNode.php index b9a9b8b..14b85fe 100644 --- a/src/Language/AST/FieldNode.php +++ b/src/Language/AST/FieldNode.php @@ -1,4 +1,7 @@ new ExecutableDefinitions(), - UniqueOperationNames::class => new UniqueOperationNames(), - LoneAnonymousOperation::class => new LoneAnonymousOperation(), - KnownTypeNames::class => new KnownTypeNames(), - FragmentsOnCompositeTypes::class => new FragmentsOnCompositeTypes(), - VariablesAreInputTypes::class => new VariablesAreInputTypes(), - ScalarLeafs::class => new ScalarLeafs(), - FieldsOnCorrectType::class => new FieldsOnCorrectType(), - UniqueFragmentNames::class => new UniqueFragmentNames(), - KnownFragmentNames::class => new KnownFragmentNames(), - NoUnusedFragments::class => new NoUnusedFragments(), - PossibleFragmentSpreads::class => new PossibleFragmentSpreads(), - NoFragmentCycles::class => new NoFragmentCycles(), - UniqueVariableNames::class => new UniqueVariableNames(), - NoUndefinedVariables::class => new NoUndefinedVariables(), - NoUnusedVariables::class => new NoUnusedVariables(), - KnownDirectives::class => new KnownDirectives(), - UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(), - KnownArgumentNames::class => new KnownArgumentNames(), - UniqueArgumentNames::class => new UniqueArgumentNames(), - ValuesOfCorrectType::class => new ValuesOfCorrectType(), - ProvidedNonNullArguments::class => new ProvidedNonNullArguments(), + ExecutableDefinitions::class => new ExecutableDefinitions(), + UniqueOperationNames::class => new UniqueOperationNames(), + LoneAnonymousOperation::class => new LoneAnonymousOperation(), + KnownTypeNames::class => new KnownTypeNames(), + FragmentsOnCompositeTypes::class => new FragmentsOnCompositeTypes(), + VariablesAreInputTypes::class => new VariablesAreInputTypes(), + ScalarLeafs::class => new ScalarLeafs(), + FieldsOnCorrectType::class => new FieldsOnCorrectType(), + UniqueFragmentNames::class => new UniqueFragmentNames(), + KnownFragmentNames::class => new KnownFragmentNames(), + NoUnusedFragments::class => new NoUnusedFragments(), + PossibleFragmentSpreads::class => new PossibleFragmentSpreads(), + NoFragmentCycles::class => new NoFragmentCycles(), + UniqueVariableNames::class => new UniqueVariableNames(), + NoUndefinedVariables::class => new NoUndefinedVariables(), + NoUnusedVariables::class => new NoUnusedVariables(), + KnownDirectives::class => new KnownDirectives(), + UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(), + KnownArgumentNames::class => new KnownArgumentNames(), + UniqueArgumentNames::class => new UniqueArgumentNames(), + ValuesOfCorrectType::class => new ValuesOfCorrectType(), + ProvidedNonNullArguments::class => new ProvidedNonNullArguments(), VariablesDefaultValueAllowed::class => new VariablesDefaultValueAllowed(), - VariablesInAllowedPosition::class => new VariablesInAllowedPosition(), + VariablesInAllowedPosition::class => new VariablesInAllowedPosition(), OverlappingFieldsCanBeMerged::class => new OverlappingFieldsCanBeMerged(), - UniqueInputFieldNames::class => new UniqueInputFieldNames(), + UniqueInputFieldNames::class => new UniqueInputFieldNames(), ]; } @@ -151,7 +159,7 @@ class DocumentValidator } /** - * @return array + * @return QuerySecurityRule[] */ public static function securityRules() { @@ -159,16 +167,36 @@ class DocumentValidator // When custom security rule is required - it should be just added via DocumentValidator::addRule(); // TODO: deprecate this - if (null === self::$securityRules) { + if (self::$securityRules === null) { self::$securityRules = [ DisableIntrospection::class => new DisableIntrospection(DisableIntrospection::DISABLED), // DEFAULT DISABLED - QueryDepth::class => new QueryDepth(QueryDepth::DISABLED), // default disabled - QueryComplexity::class => new QueryComplexity(QueryComplexity::DISABLED), // default disabled + QueryDepth::class => new QueryDepth(QueryDepth::DISABLED), // default disabled + QueryComplexity::class => new QueryComplexity(QueryComplexity::DISABLED), // default disabled ]; } + return self::$securityRules; } + /** + * This uses a specialized visitor which runs multiple visitors in parallel, + * while maintaining the visitor skip and break API. + * + * @param ValidationRule[] $rules + * @return Error[] + */ + public static function visitUsingRules(Schema $schema, TypeInfo $typeInfo, DocumentNode $documentNode, array $rules) + { + $context = new ValidationContext($schema, $documentNode, $typeInfo); + $visitors = []; + foreach ($rules as $rule) { + $visitors[] = $rule->getVisitor($context); + } + Visitor::visit($documentNode, Visitor::visitWithTypeInfo($typeInfo, Visitor::visitInParallel($visitors))); + + return $context->getErrors(); + } + /** * Returns global validation rule by name. Standard rules are named by class name, so * example usage for such rules: @@ -177,7 +205,7 @@ class DocumentValidator * * @api * @param string $name - * @return AbstractValidationRule + * @return ValidationRule */ public static function getRule($name) { @@ -187,17 +215,17 @@ class DocumentValidator return $rules[$name]; } - $name = "GraphQL\\Validator\\Rules\\$name"; - return isset($rules[$name]) ? $rules[$name] : null ; + $name = sprintf('GraphQL\\Validator\\Rules\\%s', $name); + + return $rules[$name] ?? null; } /** * Add rule to list of global validation rules * * @api - * @param AbstractValidationRule $rule */ - public static function addRule(AbstractValidationRule $rule) + public static function addRule(ValidationRule $rule) { self::$rules[$rule->getName()] = $rule; } @@ -205,7 +233,12 @@ class DocumentValidator public static function isError($value) { return is_array($value) - ? count(array_filter($value, function($item) { return $item instanceof \Exception || $item instanceof \Throwable;})) === count($value) + ? count(array_filter( + $value, + function ($item) { + return $item instanceof \Exception || $item instanceof \Throwable; + } + )) === count($value) : ($value instanceof \Exception || $value instanceof \Throwable); } @@ -216,6 +249,7 @@ class DocumentValidator } else { $arr[] = $items; } + return $arr; } @@ -230,33 +264,13 @@ class DocumentValidator public static function isValidLiteralValue(Type $type, $valueNode) { $emptySchema = new Schema([]); - $emptyDoc = new DocumentNode(['definitions' => []]); - $typeInfo = new TypeInfo($emptySchema, $type); - $context = new ValidationContext($emptySchema, $emptyDoc, $typeInfo); - $validator = new ValuesOfCorrectType(); - $visitor = $validator->getVisitor($context); + $emptyDoc = new DocumentNode(['definitions' => []]); + $typeInfo = new TypeInfo($emptySchema, $type); + $context = new ValidationContext($emptySchema, $emptyDoc, $typeInfo); + $validator = new ValuesOfCorrectType(); + $visitor = $validator->getVisitor($context); Visitor::visit($valueNode, Visitor::visitWithTypeInfo($typeInfo, $visitor)); - return $context->getErrors(); - } - /** - * This uses a specialized visitor which runs multiple visitors in parallel, - * while maintaining the visitor skip and break API. - * - * @param Schema $schema - * @param TypeInfo $typeInfo - * @param DocumentNode $documentNode - * @param AbstractValidationRule[] $rules - * @return array - */ - public static function visitUsingRules(Schema $schema, TypeInfo $typeInfo, DocumentNode $documentNode, array $rules) - { - $context = new ValidationContext($schema, $documentNode, $typeInfo); - $visitors = []; - foreach ($rules as $rule) { - $visitors[] = $rule->getVisitor($context); - } - Visitor::visit($documentNode, Visitor::visitWithTypeInfo($typeInfo, Visitor::visitInParallel($visitors))); return $context->getErrors(); } } diff --git a/src/Validator/Rules/CustomValidationRule.php b/src/Validator/Rules/CustomValidationRule.php index 5ccc606..83101a1 100644 --- a/src/Validator/Rules/CustomValidationRule.php +++ b/src/Validator/Rules/CustomValidationRule.php @@ -1,26 +1,30 @@ name = $name; + $this->name = $name; $this->visitorFn = $visitorFn; } /** - * @param ValidationContext $context * @return Error[] */ public function getVisitor(ValidationContext $context) { $fn = $this->visitorFn; + return $fn($context); } } diff --git a/src/Validator/Rules/DisableIntrospection.php b/src/Validator/Rules/DisableIntrospection.php index dec9f37..bdb7a7c 100644 --- a/src/Validator/Rules/DisableIntrospection.php +++ b/src/Validator/Rules/DisableIntrospection.php @@ -1,4 +1,7 @@ isEnabled = $enabled; } - static function introspectionDisabledMessage() + public function getVisitor(ValidationContext $context) + { + return $this->invokeIfNeeded( + $context, + [ + NodeKind::FIELD => function (FieldNode $node) use ($context) { + if ($node->name->value !== '__type' && $node->name->value !== '__schema') { + return; + } + + $context->reportError(new Error( + static::introspectionDisabledMessage(), + [$node] + )); + }, + ] + ); + } + + public static function introspectionDisabledMessage() { return 'GraphQL introspection is not allowed, but the query contained __schema or __type'; } @@ -30,21 +54,4 @@ class DisableIntrospection extends AbstractQuerySecurity { return $this->isEnabled !== static::DISABLED; } - - public function getVisitor(ValidationContext $context) - { - return $this->invokeIfNeeded( - $context, - [ - NodeKind::FIELD => function (FieldNode $node) use ($context) { - if ($node->name->value === '__type' || $node->name->value === '__schema') { - $context->reportError(new Error( - static::introspectionDisabledMessage(), - [$node] - )); - } - } - ] - ); - } } diff --git a/src/Validator/Rules/ExecutableDefinitions.php b/src/Validator/Rules/ExecutableDefinitions.php index f512d6d..325b320 100644 --- a/src/Validator/Rules/ExecutableDefinitions.php +++ b/src/Validator/Rules/ExecutableDefinitions.php @@ -1,4 +1,7 @@ function (DocumentNode $node) use ($context) { /** @var Node $definition */ foreach ($node->definitions as $definition) { - if ( - !$definition instanceof OperationDefinitionNode && - !$definition instanceof FragmentDefinitionNode + if ($definition instanceof OperationDefinitionNode || + $definition instanceof FragmentDefinitionNode ) { - $context->reportError(new Error( - self::nonExecutableDefinitionMessage($definition->name->value), - [$definition->name] - )); + continue; } + + $context->reportError(new Error( + self::nonExecutableDefinitionMessage($definition->name->value), + [$definition->name] + )); } return Visitor::skipNode(); - } + }, ]; } + + public static function nonExecutableDefinitionMessage($defName) + { + return sprintf('The "%s" definition is not executable.', $defName); + } } diff --git a/src/Validator/Rules/FieldsOnCorrectType.php b/src/Validator/Rules/FieldsOnCorrectType.php index 1244a72..104849c 100644 --- a/src/Validator/Rules/FieldsOnCorrectType.php +++ b/src/Validator/Rules/FieldsOnCorrectType.php @@ -1,4 +1,7 @@ function(FieldNode $node) use ($context) { + NodeKind::FIELD => function (FieldNode $node) use ($context) { $type = $context->getParentType(); - if ($type) { - $fieldDef = $context->getFieldDef(); - if (!$fieldDef) { - // This isn't valid. Let's find suggestions, if any. - $schema = $context->getSchema(); - $fieldName = $node->name->value; - // First determine if there are any suggested types to condition on. - $suggestedTypeNames = $this->getSuggestedTypeNames( - $schema, - $type, - $fieldName - ); - // If there are no suggested types, then perhaps this was a typo? - $suggestedFieldNames = $suggestedTypeNames - ? [] - : $this->getSuggestedFieldNames( - $schema, - $type, - $fieldName - ); - - // Report an error, including helpful suggestions. - $context->reportError(new Error( - static::undefinedFieldMessage( - $node->name->value, - $type->name, - $suggestedTypeNames, - $suggestedFieldNames - ), - [$node] - )); - } + if (! $type) { + return; } - } + + $fieldDef = $context->getFieldDef(); + if ($fieldDef) { + return; + } + + // This isn't valid. Let's find suggestions, if any. + $schema = $context->getSchema(); + $fieldName = $node->name->value; + // First determine if there are any suggested types to condition on. + $suggestedTypeNames = $this->getSuggestedTypeNames( + $schema, + $type, + $fieldName + ); + // If there are no suggested types, then perhaps this was a typo? + $suggestedFieldNames = $suggestedTypeNames + ? [] + : $this->getSuggestedFieldNames( + $schema, + $type, + $fieldName + ); + + // Report an error, including helpful suggestions. + $context->reportError(new Error( + static::undefinedFieldMessage( + $node->name->value, + $type->name, + $suggestedTypeNames, + $suggestedFieldNames + ), + [$node] + )); + }, ]; } @@ -75,32 +72,31 @@ class FieldsOnCorrectType extends AbstractValidationRule * suggest them, sorted by how often the type is referenced, starting * with Interfaces. * - * @param Schema $schema - * @param $type - * @param string $fieldName - * @return array + * @param ObjectType|InterfaceType $type + * @param string $fieldName + * @return string[] */ private function getSuggestedTypeNames(Schema $schema, $type, $fieldName) { if (Type::isAbstractType($type)) { $suggestedObjectTypes = []; - $interfaceUsageCount = []; + $interfaceUsageCount = []; - foreach($schema->getPossibleTypes($type) as $possibleType) { + foreach ($schema->getPossibleTypes($type) as $possibleType) { $fields = $possibleType->getFields(); - if (!isset($fields[$fieldName])) { + if (! isset($fields[$fieldName])) { continue; } // This object type defines this field. $suggestedObjectTypes[] = $possibleType->name; - foreach($possibleType->getInterfaces() as $possibleInterface) { + foreach ($possibleType->getInterfaces() as $possibleInterface) { $fields = $possibleInterface->getFields(); - if (!isset($fields[$fieldName])) { + if (! isset($fields[$fieldName])) { continue; } // This interface type defines this field. $interfaceUsageCount[$possibleInterface->name] = - !isset($interfaceUsageCount[$possibleInterface->name]) + ! isset($interfaceUsageCount[$possibleInterface->name]) ? 0 : $interfaceUsageCount[$possibleInterface->name] + 1; } @@ -122,18 +118,47 @@ class FieldsOnCorrectType extends AbstractValidationRule * For the field name provided, determine if there are any similar field names * that may be the result of a typo. * - * @param Schema $schema - * @param $type - * @param string $fieldName + * @param ObjectType|InterfaceType $type + * @param string $fieldName * @return array|string[] */ private function getSuggestedFieldNames(Schema $schema, $type, $fieldName) { if ($type instanceof ObjectType || $type instanceof InterfaceType) { $possibleFieldNames = array_keys($type->getFields()); + return Utils::suggestionList($fieldName, $possibleFieldNames); } + // Otherwise, must be a Union type, which does not define fields. return []; } + + /** + * @param string $fieldName + * @param string $type + * @param string[] $suggestedTypeNames + * @param string[] $suggestedFieldNames + * @return string + */ + public static function undefinedFieldMessage( + $fieldName, + $type, + array $suggestedTypeNames, + array $suggestedFieldNames + ) { + $message = sprintf('Cannot query field "%s" on type "%s".', $fieldName, $type); + + if ($suggestedTypeNames) { + $suggestions = Utils::quotedOrList($suggestedTypeNames); + + $message .= sprintf(' Did you mean to use an inline fragment on %s?', $suggestions); + } elseif (! empty($suggestedFieldNames)) { + $suggestions = Utils::quotedOrList($suggestedFieldNames); + + $message .= sprintf(' Did you mean %s?', $suggestions); + } + + return $message; + } } diff --git a/src/Validator/Rules/FragmentsOnCompositeTypes.php b/src/Validator/Rules/FragmentsOnCompositeTypes.php index f2731d0..e6f8f16 100644 --- a/src/Validator/Rules/FragmentsOnCompositeTypes.php +++ b/src/Validator/Rules/FragmentsOnCompositeTypes.php @@ -1,4 +1,7 @@ function(InlineFragmentNode $node) use ($context) { - if ($node->typeCondition) { - $type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition); - if ($type && !Type::isCompositeType($type)) { - $context->reportError(new Error( - static::inlineFragmentOnNonCompositeErrorMessage($type), - [$node->typeCondition] - )); - } + NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) use ($context) { + if (! $node->typeCondition) { + return; } + + $type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition); + if (! $type || Type::isCompositeType($type)) { + return; + } + + $context->reportError(new Error( + static::inlineFragmentOnNonCompositeErrorMessage($type), + [$node->typeCondition] + )); }, - NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) use ($context) { + NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) { $type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition); - if ($type && !Type::isCompositeType($type)) { - $context->reportError(new Error( - static::fragmentOnNonCompositeErrorMessage($node->name->value, Printer::doPrint($node->typeCondition)), - [$node->typeCondition] - )); + if (! $type || Type::isCompositeType($type)) { + return; } - } + + $context->reportError(new Error( + static::fragmentOnNonCompositeErrorMessage( + $node->name->value, + Printer::doPrint($node->typeCondition) + ), + [$node->typeCondition] + )); + }, ]; } + + public static function inlineFragmentOnNonCompositeErrorMessage($type) + { + return sprintf('Fragment cannot condition on non composite type "%s".', $type); + } + + public static function fragmentOnNonCompositeErrorMessage($fragName, $type) + { + return sprintf('Fragment "%s" cannot condition on non composite type "%s".', $fragName, $type); + } } diff --git a/src/Validator/Rules/KnownArgumentNames.php b/src/Validator/Rules/KnownArgumentNames.php index 15a77ab..bbe3a2f 100644 --- a/src/Validator/Rules/KnownArgumentNames.php +++ b/src/Validator/Rules/KnownArgumentNames.php @@ -1,4 +1,7 @@ function(ArgumentNode $node, $key, $parent, $path, $ancestors) use ($context) { + NodeKind::ARGUMENT => function (ArgumentNode $node, $key, $parent, $path, $ancestors) use ($context) { $argDef = $context->getArgument(); - if (!$argDef) { - $argumentOf = $ancestors[count($ancestors) - 1]; - if ($argumentOf->kind === NodeKind::FIELD) { - $fieldDef = $context->getFieldDef(); - $parentType = $context->getParentType(); - if ($fieldDef && $parentType) { - $context->reportError(new Error( - self::unknownArgMessage( + if ($argDef !== null) { + return; + } + + $argumentOf = $ancestors[count($ancestors) - 1]; + if ($argumentOf->kind === NodeKind::FIELD) { + $fieldDef = $context->getFieldDef(); + $parentType = $context->getParentType(); + if ($fieldDef && $parentType) { + $context->reportError(new Error( + self::unknownArgMessage( + $node->name->value, + $fieldDef->name, + $parentType->name, + Utils::suggestionList( $node->name->value, - $fieldDef->name, - $parentType->name, - Utils::suggestionList( - $node->name->value, - array_map(function ($arg) { return $arg->name; }, $fieldDef->args) + array_map( + function ($arg) { + return $arg->name; + }, + $fieldDef->args ) - ), - [$node] - )); - } - } else if ($argumentOf->kind === NodeKind::DIRECTIVE) { - $directive = $context->getDirective(); - if ($directive) { - $context->reportError(new Error( - self::unknownDirectiveArgMessage( + ) + ), + [$node] + )); + } + } elseif ($argumentOf->kind === NodeKind::DIRECTIVE) { + $directive = $context->getDirective(); + if ($directive) { + $context->reportError(new Error( + self::unknownDirectiveArgMessage( + $node->name->value, + $directive->name, + Utils::suggestionList( $node->name->value, - $directive->name, - Utils::suggestionList( - $node->name->value, - array_map(function ($arg) { return $arg->name; }, $directive->args) + array_map( + function ($arg) { + return $arg->name; + }, + $directive->args ) - ), - [$node] - )); - } + ) + ), + [$node] + )); } } - } + }, ]; } + + /** + * @param string[] $suggestedArgs + */ + public static function unknownArgMessage($argName, $fieldName, $typeName, array $suggestedArgs) + { + $message = sprintf('Unknown argument "%s" on field "%s" of type "%s".', $argName, $fieldName, $typeName); + if (! empty($suggestedArgs)) { + $message .= sprintf(' Did you mean %s?', Utils::quotedOrList($suggestedArgs)); + } + + return $message; + } + + /** + * @param string[] $suggestedArgs + */ + public static function unknownDirectiveArgMessage($argName, $directiveName, array $suggestedArgs) + { + $message = sprintf('Unknown argument "%s" on directive "@%s".', $argName, $directiveName); + if (! empty($suggestedArgs)) { + $message .= sprintf(' Did you mean %s?', Utils::quotedOrList($suggestedArgs)); + } + + return $message; + } } diff --git a/src/Validator/Rules/KnownDirectives.php b/src/Validator/Rules/KnownDirectives.php index 4ec3a01..ecccafd 100644 --- a/src/Validator/Rules/KnownDirectives.php +++ b/src/Validator/Rules/KnownDirectives.php @@ -1,4 +1,7 @@ reportError(new Error( self::unknownDirectiveMessage($node->name->value), [$node] )); + return; } $candidateLocation = $this->getDirectiveLocationForASTPath($ancestors); - if (!$candidateLocation) { + if (! $candidateLocation) { $context->reportError(new Error( self::misplacedDirectiveMessage($node->name->value, $node->type), [$node] )); - } else if (!in_array($candidateLocation, $directiveDef->locations)) { + } elseif (! in_array($candidateLocation, $directiveDef->locations)) { $context->reportError(new Error( self::misplacedDirectiveMessage($node->name->value, $candidateLocation), - [ $node ] + [$node] )); } - } + }, ]; } + public static function unknownDirectiveMessage($directiveName) + { + return sprintf('Unknown directive "%s".', $directiveName); + } + + /** + * @param (Node|NodeList)[] $ancestors + */ private function getDirectiveLocationForASTPath(array $ancestors) { $appliedTo = $ancestors[count($ancestors) - 1]; switch ($appliedTo->kind) { case NodeKind::OPERATION_DEFINITION: switch ($appliedTo->operation) { - case 'query': return DirectiveLocation::QUERY; - case 'mutation': return DirectiveLocation::MUTATION; - case 'subscription': return DirectiveLocation::SUBSCRIPTION; + case 'query': + return DirectiveLocation::QUERY; + case 'mutation': + return DirectiveLocation::MUTATION; + case 'subscription': + return DirectiveLocation::SUBSCRIPTION; } break; case NodeKind::FIELD: @@ -101,9 +109,15 @@ class KnownDirectives extends AbstractValidationRule return DirectiveLocation::INPUT_OBJECT; case NodeKind::INPUT_VALUE_DEFINITION: $parentNode = $ancestors[count($ancestors) - 3]; + return $parentNode instanceof InputObjectTypeDefinitionNode ? DirectiveLocation::INPUT_FIELD_DEFINITION : DirectiveLocation::ARGUMENT_DEFINITION; } } + + public static function misplacedDirectiveMessage($directiveName, $location) + { + return sprintf('Directive "%s" may not be used on "%s".', $directiveName, $location); + } } diff --git a/src/Validator/Rules/KnownFragmentNames.php b/src/Validator/Rules/KnownFragmentNames.php index ad5bc27..1b2b4e1 100644 --- a/src/Validator/Rules/KnownFragmentNames.php +++ b/src/Validator/Rules/KnownFragmentNames.php @@ -1,31 +1,40 @@ function(FragmentSpreadNode $node) use ($context) { + NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) use ($context) { $fragmentName = $node->name->value; - $fragment = $context->getFragment($fragmentName); - if (!$fragment) { - $context->reportError(new Error( - self::unknownFragmentMessage($fragmentName), - [$node->name] - )); + $fragment = $context->getFragment($fragmentName); + if ($fragment) { + return; } - } + + $context->reportError(new Error( + self::unknownFragmentMessage($fragmentName), + [$node->name] + )); + }, ]; } + + /** + * @param string $fragName + */ + public static function unknownFragmentMessage($fragName) + { + return sprintf('Unknown fragment "%s".', $fragName); + } } diff --git a/src/Validator/Rules/KnownTypeNames.php b/src/Validator/Rules/KnownTypeNames.php index 935b3ad..4700103 100644 --- a/src/Validator/Rules/KnownTypeNames.php +++ b/src/Validator/Rules/KnownTypeNames.php @@ -1,4 +1,7 @@ $skip, - NodeKind::INTERFACE_TYPE_DEFINITION => $skip, - NodeKind::UNION_TYPE_DEFINITION => $skip, + NodeKind::OBJECT_TYPE_DEFINITION => $skip, + NodeKind::INTERFACE_TYPE_DEFINITION => $skip, + NodeKind::UNION_TYPE_DEFINITION => $skip, NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $skip, - NodeKind::NAMED_TYPE => function(NamedTypeNode $node) use ($context) { - $schema = $context->getSchema(); + NodeKind::NAMED_TYPE => function (NamedTypeNode $node) use ($context) { + $schema = $context->getSchema(); $typeName = $node->name->value; - $type = $schema->getType($typeName); - if (!$type) { - $context->reportError(new Error( - self::unknownTypeMessage( - $typeName, - Utils::suggestionList($typeName, array_keys($schema->getTypeMap())) - ), [$node]) - ); + $type = $schema->getType($typeName); + if ($type !== null) { + return; } - } + + $context->reportError(new Error( + self::unknownTypeMessage( + $typeName, + Utils::suggestionList($typeName, array_keys($schema->getTypeMap())) + ), + [$node] + )); + }, ]; } + + /** + * @param string $type + * @param string[] $suggestedTypes + */ + public static function unknownTypeMessage($type, array $suggestedTypes) + { + $message = sprintf('Unknown type "%s".', $type); + if (! empty($suggestedTypes)) { + $suggestions = Utils::quotedOrList($suggestedTypes); + + $message .= sprintf(' Did you mean %s?', $suggestions); + } + + return $message; + } } diff --git a/src/Validator/Rules/LoneAnonymousOperation.php b/src/Validator/Rules/LoneAnonymousOperation.php index 7f848ef..631b6a8 100644 --- a/src/Validator/Rules/LoneAnonymousOperation.php +++ b/src/Validator/Rules/LoneAnonymousOperation.php @@ -1,4 +1,7 @@ function(DocumentNode $node) use (&$operationCount) { + NodeKind::DOCUMENT => function (DocumentNode $node) use (&$operationCount) { $tmp = Utils::filter( $node->definitions, function ($definition) { return $definition->kind === NodeKind::OPERATION_DEFINITION; } ); + $operationCount = count($tmp); }, - NodeKind::OPERATION_DEFINITION => function(OperationDefinitionNode $node) use (&$operationCount, $context) { - if (!$node->name && $operationCount > 1) { - $context->reportError( - new Error(self::anonOperationNotAloneMessage(), [$node]) - ); + NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) use ( + &$operationCount, + $context + ) { + if ($node->name || $operationCount <= 1) { + return; } - } + + $context->reportError( + new Error(self::anonOperationNotAloneMessage(), [$node]) + ); + }, ]; } + + public static function anonOperationNotAloneMessage() + { + return 'This anonymous operation must be the only defined operation.'; + } } diff --git a/src/Validator/Rules/NoFragmentCycles.php b/src/Validator/Rules/NoFragmentCycles.php index df81b77..0b0a1e6 100644 --- a/src/Validator/Rules/NoFragmentCycles.php +++ b/src/Validator/Rules/NoFragmentCycles.php @@ -1,25 +1,33 @@ function () { return Visitor::skipNode(); }, - NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) { - if (!isset($this->visitedFrags[$node->name->value])) { + NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) { + if (! isset($this->visitedFrags[$node->name->value])) { $this->detectCycleRecursive($node, $context); } + return Visitor::skipNode(); - } + }, ]; } private function detectCycleRecursive(FragmentDefinitionNode $fragment, ValidationContext $context) { - $fragmentName = $fragment->name->value; + $fragmentName = $fragment->name->value; $this->visitedFrags[$fragmentName] = true; $spreadNodes = $context->getFragmentSpreads($fragment); @@ -63,7 +72,7 @@ class NoFragmentCycles extends AbstractValidationRule for ($i = 0; $i < count($spreadNodes); $i++) { $spreadNode = $spreadNodes[$i]; $spreadName = $spreadNode->name->value; - $cycleIndex = isset($this->spreadPathIndexByName[$spreadName]) ? $this->spreadPathIndexByName[$spreadName] : null; + $cycleIndex = $this->spreadPathIndexByName[$spreadName] ?? null; if ($cycleIndex === null) { $this->spreadPath[] = $spreadNode; @@ -76,7 +85,7 @@ class NoFragmentCycles extends AbstractValidationRule array_pop($this->spreadPath); } else { $cyclePath = array_slice($this->spreadPath, $cycleIndex); - $nodes = $cyclePath; + $nodes = $cyclePath; if (is_array($spreadNode)) { $nodes = array_merge($nodes, $spreadNode); @@ -87,9 +96,12 @@ class NoFragmentCycles extends AbstractValidationRule $context->reportError(new Error( self::cycleErrorMessage( $spreadName, - Utils::map($cyclePath, function ($s) { - return $s->name->value; - }) + Utils::map( + $cyclePath, + function ($s) { + return $s->name->value; + } + ) ), $nodes )); @@ -98,4 +110,16 @@ class NoFragmentCycles extends AbstractValidationRule $this->spreadPathIndexByName[$fragmentName] = null; } + + /** + * @param string[] $spreadNames + */ + public static function cycleErrorMessage($fragName, array $spreadNames = []) + { + return sprintf( + 'Cannot spread fragment "%s" within itself%s.', + $fragName, + ! empty($spreadNames) ? ' via ' . implode(', ', $spreadNames) : '' + ); + } } diff --git a/src/Validator/Rules/NoUndefinedVariables.php b/src/Validator/Rules/NoUndefinedVariables.php index 42aa3cc..af7bfd4 100644 --- a/src/Validator/Rules/NoUndefinedVariables.php +++ b/src/Validator/Rules/NoUndefinedVariables.php @@ -1,4 +1,7 @@ [ - 'enter' => function() use (&$variableNameDefined) { + 'enter' => function () use (&$variableNameDefined) { $variableNameDefined = []; }, - 'leave' => function(OperationDefinitionNode $operation) use (&$variableNameDefined, $context) { + 'leave' => function (OperationDefinitionNode $operation) use (&$variableNameDefined, $context) { $usages = $context->getRecursiveVariableUsages($operation); foreach ($usages as $usage) { - $node = $usage['node']; + $node = $usage['node']; $varName = $node->name->value; - if (empty($variableNameDefined[$varName])) { - $context->reportError(new Error( - self::undefinedVarMessage( - $varName, - $operation->name ? $operation->name->value : null - ), - [ $node, $operation ] - )); + if (! empty($variableNameDefined[$varName])) { + continue; } + + $context->reportError(new Error( + self::undefinedVarMessage( + $varName, + $operation->name ? $operation->name->value : null + ), + [$node, $operation] + )); } - } + }, ], - NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $def) use (&$variableNameDefined) { + NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $def) use (&$variableNameDefined) { $variableNameDefined[$def->variable->name->value] = true; - } + }, ]; } + + public static function undefinedVarMessage($varName, $opName = null) + { + return $opName + ? sprintf('Variable "$%s" is not defined by operation "%s".', $varName, $opName) + : sprintf('Variable "$%s" is not defined.', $varName); + } } diff --git a/src/Validator/Rules/NoUnusedFragments.php b/src/Validator/Rules/NoUnusedFragments.php index 2168586..d1cd366 100644 --- a/src/Validator/Rules/NoUnusedFragments.php +++ b/src/Validator/Rules/NoUnusedFragments.php @@ -1,39 +1,43 @@ operationDefs = []; - $this->fragmentDefs = []; + $this->fragmentDefs = []; return [ - NodeKind::OPERATION_DEFINITION => function($node) { + NodeKind::OPERATION_DEFINITION => function ($node) { $this->operationDefs[] = $node; + return Visitor::skipNode(); }, - NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $def) { + NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $def) { $this->fragmentDefs[] = $def; + return Visitor::skipNode(); }, - NodeKind::DOCUMENT => [ - 'leave' => function() use ($context) { + NodeKind::DOCUMENT => [ + 'leave' => function () use ($context) { $fragmentNameUsed = []; foreach ($this->operationDefs as $operation) { @@ -44,15 +48,22 @@ class NoUnusedFragments extends AbstractValidationRule foreach ($this->fragmentDefs as $fragmentDef) { $fragName = $fragmentDef->name->value; - if (empty($fragmentNameUsed[$fragName])) { - $context->reportError(new Error( - self::unusedFragMessage($fragName), - [ $fragmentDef ] - )); + if (! empty($fragmentNameUsed[$fragName])) { + continue; } + + $context->reportError(new Error( + self::unusedFragMessage($fragName), + [$fragmentDef] + )); } - } - ] + }, + ], ]; } + + public static function unusedFragMessage($fragName) + { + return sprintf('Fragment "%s" is never used.', $fragName); + } } diff --git a/src/Validator/Rules/NoUnusedVariables.php b/src/Validator/Rules/NoUnusedVariables.php index c004623..e8f7ff3 100644 --- a/src/Validator/Rules/NoUnusedVariables.php +++ b/src/Validator/Rules/NoUnusedVariables.php @@ -1,20 +1,19 @@ [ - 'enter' => function() { + 'enter' => function () { $this->variableDefs = []; }, - 'leave' => function(OperationDefinitionNode $operation) use ($context) { + 'leave' => function (OperationDefinitionNode $operation) use ($context) { $variableNameUsed = []; - $usages = $context->getRecursiveVariableUsages($operation); - $opName = $operation->name ? $operation->name->value : null; + $usages = $context->getRecursiveVariableUsages($operation); + $opName = $operation->name ? $operation->name->value : null; foreach ($usages as $usage) { - $node = $usage['node']; + $node = $usage['node']; $variableNameUsed[$node->name->value] = true; } foreach ($this->variableDefs as $variableDef) { $variableName = $variableDef->variable->name->value; - if (empty($variableNameUsed[$variableName])) { - $context->reportError(new Error( - self::unusedVariableMessage($variableName, $opName), - [$variableDef] - )); + if (! empty($variableNameUsed[$variableName])) { + continue; } + + $context->reportError(new Error( + self::unusedVariableMessage($variableName, $opName), + [$variableDef] + )); } - } + }, ], - NodeKind::VARIABLE_DEFINITION => function($def) { + NodeKind::VARIABLE_DEFINITION => function ($def) { $this->variableDefs[] = $def; - } + }, ]; } + + public static function unusedVariableMessage($varName, $opName = null) + { + return $opName + ? sprintf('Variable "$%s" is never used in operation "%s".', $varName, $opName) + : sprintf('Variable "$%s" is never used.', $varName); + } } diff --git a/src/Validator/Rules/OverlappingFieldsCanBeMerged.php b/src/Validator/Rules/OverlappingFieldsCanBeMerged.php index 7ab2838..9a4c56b 100644 --- a/src/Validator/Rules/OverlappingFieldsCanBeMerged.php +++ b/src/Validator/Rules/OverlappingFieldsCanBeMerged.php @@ -1,4 +1,7 @@ comparedFragmentPairs = new PairSet(); + $this->comparedFragmentPairs = new PairSet(); $this->cachedFieldsAndFragmentNames = new \SplObjectStorage(); return [ - NodeKind::SELECTION_SET => function(SelectionSetNode $selectionSet) use ($context) { + NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context) { $conflicts = $this->findConflictsWithinSelectionSet( $context, $context->getParentType(), @@ -74,20 +66,112 @@ class OverlappingFieldsCanBeMerged extends AbstractValidationRule ); foreach ($conflicts as $conflict) { - $responseName = $conflict[0][0]; - $reason = $conflict[0][1]; - $fields1 = $conflict[1]; - $fields2 = $conflict[2]; + [[$responseName, $reason], $fields1, $fields2] = $conflict; $context->reportError(new Error( self::fieldsConflictMessage($responseName, $reason), array_merge($fields1, $fields2) )); } - } + }, ]; } + /** + * Find all conflicts found "within" a selection set, including those found + * via spreading in fragments. Called when visiting each SelectionSet in the + * GraphQL Document. + * + * @param CompositeType $parentType + * @return mixed[] + */ + private function findConflictsWithinSelectionSet( + ValidationContext $context, + $parentType, + SelectionSetNode $selectionSet + ) { + [$fieldMap, $fragmentNames] = $this->getFieldsAndFragmentNames( + $context, + $parentType, + $selectionSet + ); + + $conflicts = []; + + // (A) Find find all conflicts "within" the fields of this selection set. + // Note: this is the *only place* `collectConflictsWithin` is called. + $this->collectConflictsWithin( + $context, + $conflicts, + $fieldMap + ); + + $fragmentNamesLength = count($fragmentNames); + if ($fragmentNamesLength !== 0) { + // (B) Then collect conflicts between these fields and those represented by + // each spread fragment name found. + $comparedFragments = []; + for ($i = 0; $i < $fragmentNamesLength; $i++) { + $this->collectConflictsBetweenFieldsAndFragment( + $context, + $conflicts, + $comparedFragments, + false, + $fieldMap, + $fragmentNames[$i] + ); + // (C) Then compare this fragment with all other fragments found in this + // selection set to collect conflicts between fragments spread together. + // This compares each item in the list of fragment names to every other item + // in that same list (except for itself). + for ($j = $i + 1; $j < $fragmentNamesLength; $j++) { + $this->collectConflictsBetweenFragments( + $context, + $conflicts, + false, + $fragmentNames[$i], + $fragmentNames[$j] + ); + } + } + } + + return $conflicts; + } + + /** + * Given a selection set, return the collection of fields (a mapping of response + * name to field ASTs and definitions) as well as a list of fragment names + * referenced via fragment spreads. + * + * @param CompositeType $parentType + * @return mixed[]|\SplObjectStorage + */ + private function getFieldsAndFragmentNames( + ValidationContext $context, + $parentType, + SelectionSetNode $selectionSet + ) { + if (isset($this->cachedFieldsAndFragmentNames[$selectionSet])) { + $cached = $this->cachedFieldsAndFragmentNames[$selectionSet]; + } else { + $astAndDefs = []; + $fragmentNames = []; + + $this->internalCollectFieldsAndFragmentNames( + $context, + $parentType, + $selectionSet, + $astAndDefs, + $fragmentNames + ); + $cached = [$astAndDefs, array_keys($fragmentNames)]; + $this->cachedFieldsAndFragmentNames[$selectionSet] = $cached; + } + + return $cached; + } + /** * Algorithm: * @@ -144,221 +228,271 @@ class OverlappingFieldsCanBeMerged extends AbstractValidationRule */ /** - * Find all conflicts found "within" a selection set, including those found - * via spreading in fragments. Called when visiting each SelectionSet in the - * GraphQL Document. + * Given a reference to a fragment, return the represented collection of fields + * as well as a list of nested fragment names referenced via fragment spreads. * - * @param ValidationContext $context * @param CompositeType $parentType - * @param SelectionSetNode $selectionSet - * @return array + * @param mixed[][][] $astAndDefs + * @param bool[] $fragmentNames */ - private function findConflictsWithinSelectionSet( + private function internalCollectFieldsAndFragmentNames( ValidationContext $context, $parentType, - SelectionSetNode $selectionSet) - { - list($fieldMap, $fragmentNames) = $this->getFieldsAndFragmentNames( - $context, - $parentType, - $selectionSet - ); + SelectionSetNode $selectionSet, + array &$astAndDefs, + array &$fragmentNames + ) { + foreach ($selectionSet->selections as $selection) { + switch (true) { + case $selection instanceof FieldNode: + $fieldName = $selection->name->value; + $fieldDef = null; + if ($parentType instanceof ObjectType || + $parentType instanceof InterfaceType) { + $tmp = $parentType->getFields(); + if (isset($tmp[$fieldName])) { + $fieldDef = $tmp[$fieldName]; + } + } + $responseName = $selection->alias ? $selection->alias->value : $fieldName; - $conflicts = []; + if (! isset($astAndDefs[$responseName])) { + $astAndDefs[$responseName] = []; + } + $astAndDefs[$responseName][] = [$parentType, $selection, $fieldDef]; + break; + case $selection instanceof FragmentSpreadNode: + $fragmentNames[$selection->name->value] = true; + break; + case $selection instanceof InlineFragmentNode: + $typeCondition = $selection->typeCondition; + $inlineFragmentType = $typeCondition + ? TypeInfo::typeFromAST($context->getSchema(), $typeCondition) + : $parentType; - // (A) Find find all conflicts "within" the fields of this selection set. - // Note: this is the *only place* `collectConflictsWithin` is called. - $this->collectConflictsWithin( - $context, - $conflicts, - $fieldMap - ); - - - $fragmentNamesLength = count($fragmentNames); - if ($fragmentNamesLength !== 0) { - // (B) Then collect conflicts between these fields and those represented by - // each spread fragment name found. - $comparedFragments = []; - for ($i = 0; $i < $fragmentNamesLength; $i++) { - $this->collectConflictsBetweenFieldsAndFragment( - $context, - $conflicts, - $comparedFragments, - false, - $fieldMap, - $fragmentNames[$i] - ); - // (C) Then compare this fragment with all other fragments found in this - // selection set to collect conflicts between fragments spread together. - // This compares each item in the list of fragment names to every other item - // in that same list (except for itself). - for ($j = $i + 1; $j < $fragmentNamesLength; $j++) { - $this->collectConflictsBetweenFragments( + $this->internalCollectFieldsAndFragmentNames( $context, - $conflicts, - false, - $fragmentNames[$i], - $fragmentNames[$j] + $inlineFragmentType, + $selection->selectionSet, + $astAndDefs, + $fragmentNames ); + break; + } + } + } + + /** + * Collect all Conflicts "within" one collection of fields. + * + * @param mixed[][] $conflicts + * @param mixed[][] $fieldMap + */ + private function collectConflictsWithin( + ValidationContext $context, + array &$conflicts, + array $fieldMap + ) { + // A field map is a keyed collection, where each key represents a response + // name and the value at that key is a list of all fields which provide that + // response name. For every response name, if there are multiple fields, they + // must be compared to find a potential conflict. + foreach ($fieldMap as $responseName => $fields) { + // This compares every field in the list to every other field in this list + // (except to itself). If the list only has one item, nothing needs to + // be compared. + $fieldsLength = count($fields); + if ($fieldsLength <= 1) { + continue; + } + + for ($i = 0; $i < $fieldsLength; $i++) { + for ($j = $i + 1; $j < $fieldsLength; $j++) { + $conflict = $this->findConflict( + $context, + false, // within one collection is never mutually exclusive + $responseName, + $fields[$i], + $fields[$j] + ); + if (! $conflict) { + continue; + } + + $conflicts[] = $conflict; } } } - - return $conflicts; } /** - * Collect all conflicts found between a set of fields and a fragment reference - * including via spreading in any nested fragments. + * Determines if there is a conflict between two particular fields, including + * comparing their sub-fields. * - * @param ValidationContext $context - * @param array $conflicts - * @param array $comparedFragments - * @param bool $areMutuallyExclusive - * @param array $fieldMap - * @param string $fragmentName + * @param bool $parentFieldsAreMutuallyExclusive + * @param string $responseName + * @param mixed[] $field1 + * @param mixed[] $field2 + * @return mixed[]|null */ - private function collectConflictsBetweenFieldsAndFragment( + private function findConflict( ValidationContext $context, - array &$conflicts, - array &$comparedFragments, - $areMutuallyExclusive, - array $fieldMap, - $fragmentName + $parentFieldsAreMutuallyExclusive, + $responseName, + array $field1, + array $field2 ) { - if (isset($comparedFragments[$fragmentName])) { - return; - } - $comparedFragments[$fragmentName] = true; + [$parentType1, $ast1, $def1] = $field1; + [$parentType2, $ast2, $def2] = $field2; - $fragment = $context->getFragment($fragmentName); - if (!$fragment) { - return; + // If it is known that two fields could not possibly apply at the same + // time, due to the parent types, then it is safe to permit them to diverge + // in aliased field or arguments used as they will not present any ambiguity + // by differing. + // It is known that two parent types could never overlap if they are + // different Object types. Interface or Union types might overlap - if not + // in the current state of the schema, then perhaps in some future version, + // thus may not safely diverge. + $areMutuallyExclusive = + $parentFieldsAreMutuallyExclusive || + ( + $parentType1 !== $parentType2 && + $parentType1 instanceof ObjectType && + $parentType2 instanceof ObjectType + ); + + // The return type for each field. + $type1 = $def1 ? $def1->getType() : null; + $type2 = $def2 ? $def2->getType() : null; + + if (! $areMutuallyExclusive) { + // Two aliases must refer to the same field. + $name1 = $ast1->name->value; + $name2 = $ast2->name->value; + if ($name1 !== $name2) { + return [ + [$responseName, sprintf('%s and %s are different fields', $name1, $name2)], + [$ast1], + [$ast2], + ]; + } + + if (! $this->sameArguments($ast1->arguments ?: [], $ast2->arguments ?: [])) { + return [ + [$responseName, 'they have differing arguments'], + [$ast1], + [$ast2], + ]; + } } - list($fieldMap2, $fragmentNames2) = $this->getReferencedFieldsAndFragmentNames( - $context, - $fragment - ); - - if ($fieldMap === $fieldMap2) { - return; + if ($type1 && $type2 && $this->doTypesConflict($type1, $type2)) { + return [ + [$responseName, sprintf('they return conflicting types %s and %s', $type1, $type2)], + [$ast1], + [$ast2], + ]; } - // (D) First collect any conflicts between the provided collection of fields - // and the collection of fields represented by the given fragment. - $this->collectConflictsBetween( - $context, - $conflicts, - $areMutuallyExclusive, - $fieldMap, - $fieldMap2 - ); - - // (E) Then collect any conflicts between the provided collection of fields - // and any fragment names found in the given fragment. - $fragmentNames2Length = count($fragmentNames2); - for ($i = 0; $i < $fragmentNames2Length; $i++) { - $this->collectConflictsBetweenFieldsAndFragment( + // Collect and compare sub-fields. Use the same "visited fragment names" list + // for both collections so fields in a fragment reference are never + // compared to themselves. + $selectionSet1 = $ast1->selectionSet; + $selectionSet2 = $ast2->selectionSet; + if ($selectionSet1 && $selectionSet2) { + $conflicts = $this->findConflictsBetweenSubSelectionSets( $context, - $conflicts, - $comparedFragments, $areMutuallyExclusive, - $fieldMap, - $fragmentNames2[$i] + Type::getNamedType($type1), + $selectionSet1, + Type::getNamedType($type2), + $selectionSet2 + ); + + return $this->subfieldConflicts( + $conflicts, + $responseName, + $ast1, + $ast2 ); } + + return null; } /** - * Collect all conflicts found between two fragments, including via spreading in - * any nested fragments. + * @param ArgumentNode[] $arguments1 + * @param ArgumentNode[] $arguments2 * - * @param ValidationContext $context - * @param array $conflicts - * @param bool $areMutuallyExclusive - * @param string $fragmentName1 - * @param string $fragmentName2 + * @return bool */ - private function collectConflictsBetweenFragments( - ValidationContext $context, - array &$conflicts, - $areMutuallyExclusive, - $fragmentName1, - $fragmentName2 - ) { - // No need to compare a fragment to itself. - if ($fragmentName1 === $fragmentName2) { - return; + private function sameArguments($arguments1, $arguments2) + { + if (count($arguments1) !== count($arguments2)) { + return false; + } + foreach ($arguments1 as $argument1) { + $argument2 = null; + foreach ($arguments2 as $argument) { + if ($argument->name->value === $argument1->name->value) { + $argument2 = $argument; + break; + } + } + if (! $argument2) { + return false; + } + + if (! $this->sameValue($argument1->value, $argument2->value)) { + return false; + } } - // Memoize so two fragments are not compared for conflicts more than once. - if ( - $this->comparedFragmentPairs->has( - $fragmentName1, - $fragmentName2, - $areMutuallyExclusive - ) - ) { - return; - } - $this->comparedFragmentPairs->add( - $fragmentName1, - $fragmentName2, - $areMutuallyExclusive - ); + return true; + } - $fragment1 = $context->getFragment($fragmentName1); - $fragment2 = $context->getFragment($fragmentName2); - if (!$fragment1 || !$fragment2) { - return; + /** + * @return bool + */ + private function sameValue(Node $value1, Node $value2) + { + return (! $value1 && ! $value2) || (Printer::doPrint($value1) === Printer::doPrint($value2)); + } + + /** + * Two types conflict if both types could not apply to a value simultaneously. + * Composite types are ignored as their individual field types will be compared + * later recursively. However List and Non-Null types must match. + * + * @return bool + */ + private function doTypesConflict(OutputType $type1, OutputType $type2) + { + if ($type1 instanceof ListOfType) { + return $type2 instanceof ListOfType ? + $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : + true; + } + if ($type2 instanceof ListOfType) { + return $type1 instanceof ListOfType ? + $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : + true; + } + if ($type1 instanceof NonNull) { + return $type2 instanceof NonNull ? + $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : + true; + } + if ($type2 instanceof NonNull) { + return $type1 instanceof NonNull ? + $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : + true; + } + if (Type::isLeafType($type1) || Type::isLeafType($type2)) { + return $type1 !== $type2; } - list($fieldMap1, $fragmentNames1) = $this->getReferencedFieldsAndFragmentNames( - $context, - $fragment1 - ); - list($fieldMap2, $fragmentNames2) = $this->getReferencedFieldsAndFragmentNames( - $context, - $fragment2 - ); - - // (F) First, collect all conflicts between these two collections of fields - // (not including any nested fragments). - $this->collectConflictsBetween( - $context, - $conflicts, - $areMutuallyExclusive, - $fieldMap1, - $fieldMap2 - ); - - // (G) Then collect conflicts between the first fragment and any nested - // fragments spread in the second fragment. - $fragmentNames2Length = count($fragmentNames2); - for ($j = 0; $j < $fragmentNames2Length; $j++) { - $this->collectConflictsBetweenFragments( - $context, - $conflicts, - $areMutuallyExclusive, - $fragmentName1, - $fragmentNames2[$j] - ); - } - - // (G) Then collect conflicts between the second fragment and any nested - // fragments spread in the first fragment. - $fragmentNames1Length = count($fragmentNames1); - for ($i = 0; $i < $fragmentNames1Length; $i++) { - $this->collectConflictsBetweenFragments( - $context, - $conflicts, - $areMutuallyExclusive, - $fragmentNames1[$i], - $fragmentName2 - ); - } + return false; } /** @@ -366,13 +500,10 @@ class OverlappingFieldsCanBeMerged extends AbstractValidationRule * via spreading in fragments. Called when determining if conflicts exist * between the sub-fields of two overlapping fields. * - * @param ValidationContext $context - * @param bool $areMutuallyExclusive + * @param bool $areMutuallyExclusive * @param CompositeType $parentType1 - * @param $selectionSet1 * @param CompositeType $parentType2 - * @param $selectionSet2 - * @return array + * @return mixed[][] */ private function findConflictsBetweenSubSelectionSets( ValidationContext $context, @@ -384,12 +515,12 @@ class OverlappingFieldsCanBeMerged extends AbstractValidationRule ) { $conflicts = []; - list($fieldMap1, $fragmentNames1) = $this->getFieldsAndFragmentNames( + [$fieldMap1, $fragmentNames1] = $this->getFieldsAndFragmentNames( $context, $parentType1, $selectionSet1 ); - list($fieldMap2, $fragmentNames2) = $this->getFieldsAndFragmentNames( + [$fieldMap2, $fragmentNames2] = $this->getFieldsAndFragmentNames( $context, $parentType2, $selectionSet2 @@ -452,48 +583,8 @@ class OverlappingFieldsCanBeMerged extends AbstractValidationRule ); } } - return $conflicts; - } - /** - * Collect all Conflicts "within" one collection of fields. - * - * @param ValidationContext $context - * @param array $conflicts - * @param array $fieldMap - */ - private function collectConflictsWithin( - ValidationContext $context, - array &$conflicts, - array $fieldMap - ) - { - // A field map is a keyed collection, where each key represents a response - // name and the value at that key is a list of all fields which provide that - // response name. For every response name, if there are multiple fields, they - // must be compared to find a potential conflict. - foreach ($fieldMap as $responseName => $fields) { - // This compares every field in the list to every other field in this list - // (except to itself). If the list only has one item, nothing needs to - // be compared. - $fieldsLength = count($fields); - if ($fieldsLength > 1) { - for ($i = 0; $i < $fieldsLength; $i++) { - for ($j = $i + 1; $j < $fieldsLength; $j++) { - $conflict = $this->findConflict( - $context, - false, // within one collection is never mutually exclusive - $responseName, - $fields[$i], - $fields[$j] - ); - if ($conflict) { - $conflicts[] = $conflict; - } - } - } - } - } + return $conflicts; } /** @@ -503,11 +594,10 @@ class OverlappingFieldsCanBeMerged extends AbstractValidationRule * provided collection of fields. This is true because this validator traverses * each individual selection set. * - * @param ValidationContext $context - * @param array $conflicts - * @param bool $parentFieldsAreMutuallyExclusive - * @param array $fieldMap1 - * @param array $fieldMap2 + * @param mixed[][] $conflicts + * @param bool $parentFieldsAreMutuallyExclusive + * @param mixed[] $fieldMap1 + * @param mixed[] $fieldMap2 */ private function collectConflictsBetween( ValidationContext $context, @@ -522,253 +612,111 @@ class OverlappingFieldsCanBeMerged extends AbstractValidationRule // maps, each field from the first field map must be compared to every field // in the second field map to find potential conflicts. foreach ($fieldMap1 as $responseName => $fields1) { - if (isset($fieldMap2[$responseName])) { - $fields2 = $fieldMap2[$responseName]; - $fields1Length = count($fields1); - $fields2Length = count($fields2); - for ($i = 0; $i < $fields1Length; $i++) { - for ($j = 0; $j < $fields2Length; $j++) { - $conflict = $this->findConflict( - $context, - $parentFieldsAreMutuallyExclusive, - $responseName, - $fields1[$i], - $fields2[$j] - ); - if ($conflict) { - $conflicts[] = $conflict; - } + if (! isset($fieldMap2[$responseName])) { + continue; + } + + $fields2 = $fieldMap2[$responseName]; + $fields1Length = count($fields1); + $fields2Length = count($fields2); + for ($i = 0; $i < $fields1Length; $i++) { + for ($j = 0; $j < $fields2Length; $j++) { + $conflict = $this->findConflict( + $context, + $parentFieldsAreMutuallyExclusive, + $responseName, + $fields1[$i], + $fields2[$j] + ); + if (! $conflict) { + continue; } + + $conflicts[] = $conflict; } } } } /** - * Determines if there is a conflict between two particular fields, including - * comparing their sub-fields. + * Collect all conflicts found between a set of fields and a fragment reference + * including via spreading in any nested fragments. * - * @param ValidationContext $context - * @param bool $parentFieldsAreMutuallyExclusive - * @param string $responseName - * @param array $field1 - * @param array $field2 - * @return array|null + * @param mixed[][] $conflicts + * @param bool[] $comparedFragments + * @param bool $areMutuallyExclusive + * @param mixed[][] $fieldMap + * @param string $fragmentName */ - private function findConflict( + private function collectConflictsBetweenFieldsAndFragment( ValidationContext $context, - $parentFieldsAreMutuallyExclusive, - $responseName, - array $field1, - array $field2 - ) - { - list($parentType1, $ast1, $def1) = $field1; - list($parentType2, $ast2, $def2) = $field2; - - // If it is known that two fields could not possibly apply at the same - // time, due to the parent types, then it is safe to permit them to diverge - // in aliased field or arguments used as they will not present any ambiguity - // by differing. - // It is known that two parent types could never overlap if they are - // different Object types. Interface or Union types might overlap - if not - // in the current state of the schema, then perhaps in some future version, - // thus may not safely diverge. - $areMutuallyExclusive = - $parentFieldsAreMutuallyExclusive || - $parentType1 !== $parentType2 && - $parentType1 instanceof ObjectType && - $parentType2 instanceof ObjectType; - - // The return type for each field. - $type1 = $def1 ? $def1->getType() : null; - $type2 = $def2 ? $def2->getType() : null; - - if (!$areMutuallyExclusive) { - // Two aliases must refer to the same field. - $name1 = $ast1->name->value; - $name2 = $ast2->name->value; - if ($name1 !== $name2) { - return [ - [$responseName, "$name1 and $name2 are different fields"], - [$ast1], - [$ast2] - ]; - } - - if (!$this->sameArguments($ast1->arguments ?: [], $ast2->arguments ?: [])) { - return [ - [$responseName, 'they have differing arguments'], - [$ast1], - [$ast2] - ]; - } - } - - if ($type1 && $type2 && $this->doTypesConflict($type1, $type2)) { - return [ - [$responseName, "they return conflicting types $type1 and $type2"], - [$ast1], - [$ast2] - ]; - } - - // Collect and compare sub-fields. Use the same "visited fragment names" list - // for both collections so fields in a fragment reference are never - // compared to themselves. - $selectionSet1 = $ast1->selectionSet; - $selectionSet2 = $ast2->selectionSet; - if ($selectionSet1 && $selectionSet2) { - $conflicts = $this->findConflictsBetweenSubSelectionSets( - $context, - $areMutuallyExclusive, - Type::getNamedType($type1), - $selectionSet1, - Type::getNamedType($type2), - $selectionSet2 - ); - return $this->subfieldConflicts( - $conflicts, - $responseName, - $ast1, - $ast2 - ); - } - - return null; - } - - /** - * @param ArgumentNode[] $arguments1 - * @param ArgumentNode[] $arguments2 - * - * @return bool - */ - private function sameArguments($arguments1, $arguments2) - { - if (count($arguments1) !== count($arguments2)) { - return false; - } - foreach ($arguments1 as $argument1) { - $argument2 = null; - foreach ($arguments2 as $argument) { - if ($argument->name->value === $argument1->name->value) { - $argument2 = $argument; - break; - } - } - if (!$argument2) { - return false; - } - - if (!$this->sameValue($argument1->value, $argument2->value)) { - return false; - } - } - - return true; - } - - /** - * @param Node $value1 - * @param Node $value2 - * @return bool - */ - private function sameValue(Node $value1, Node $value2) - { - return (!$value1 && !$value2) || (Printer::doPrint($value1) === Printer::doPrint($value2)); - } - - /** - * Two types conflict if both types could not apply to a value simultaneously. - * Composite types are ignored as their individual field types will be compared - * later recursively. However List and Non-Null types must match. - * - * @param OutputType $type1 - * @param OutputType $type2 - * @return bool - */ - private function doTypesConflict(OutputType $type1, OutputType $type2) - { - if ($type1 instanceof ListOfType) { - return $type2 instanceof ListOfType ? - $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : - true; - } - if ($type2 instanceof ListOfType) { - return $type1 instanceof ListOfType ? - $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : - true; - } - if ($type1 instanceof NonNull) { - return $type2 instanceof NonNull ? - $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : - true; - } - if ($type2 instanceof NonNull) { - return $type1 instanceof NonNull ? - $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : - true; - } - if (Type::isLeafType($type1) || Type::isLeafType($type2)) { - return $type1 !== $type2; - } - return false; - } - - /** - * Given a selection set, return the collection of fields (a mapping of response - * name to field ASTs and definitions) as well as a list of fragment names - * referenced via fragment spreads. - * - * @param ValidationContext $context - * @param CompositeType $parentType - * @param SelectionSetNode $selectionSet - * @return array - */ - private function getFieldsAndFragmentNames( - ValidationContext $context, - $parentType, - SelectionSetNode $selectionSet + array &$conflicts, + array &$comparedFragments, + $areMutuallyExclusive, + array $fieldMap, + $fragmentName ) { - if (!isset($this->cachedFieldsAndFragmentNames[$selectionSet])) { - $astAndDefs = []; - $fragmentNames = []; - - $this->internalCollectFieldsAndFragmentNames( - $context, - $parentType, - $selectionSet, - $astAndDefs, - $fragmentNames - ); - $cached = [$astAndDefs, array_keys($fragmentNames)]; - $this->cachedFieldsAndFragmentNames[$selectionSet] = $cached; - } else { - $cached = $this->cachedFieldsAndFragmentNames[$selectionSet]; + if (isset($comparedFragments[$fragmentName])) { + return; + } + $comparedFragments[$fragmentName] = true; + + $fragment = $context->getFragment($fragmentName); + if (! $fragment) { + return; + } + + [$fieldMap2, $fragmentNames2] = $this->getReferencedFieldsAndFragmentNames( + $context, + $fragment + ); + + if ($fieldMap === $fieldMap2) { + return; + } + + // (D) First collect any conflicts between the provided collection of fields + // and the collection of fields represented by the given fragment. + $this->collectConflictsBetween( + $context, + $conflicts, + $areMutuallyExclusive, + $fieldMap, + $fieldMap2 + ); + + // (E) Then collect any conflicts between the provided collection of fields + // and any fragment names found in the given fragment. + $fragmentNames2Length = count($fragmentNames2); + for ($i = 0; $i < $fragmentNames2Length; $i++) { + $this->collectConflictsBetweenFieldsAndFragment( + $context, + $conflicts, + $comparedFragments, + $areMutuallyExclusive, + $fieldMap, + $fragmentNames2[$i] + ); } - return $cached; } /** * Given a reference to a fragment, return the represented collection of fields * as well as a list of nested fragment names referenced via fragment spreads. * - * @param ValidationContext $context - * @param FragmentDefinitionNode $fragment - * @return array|object + * @return mixed[]|\SplObjectStorage */ private function getReferencedFieldsAndFragmentNames( ValidationContext $context, FragmentDefinitionNode $fragment - ) - { + ) { // Short-circuit building a type from the AST if possible. if (isset($this->cachedFieldsAndFragmentNames[$fragment->selectionSet])) { return $this->cachedFieldsAndFragmentNames[$fragment->selectionSet]; } $fragmentType = TypeInfo::typeFromAST($context->getSchema(), $fragment->typeCondition); + return $this->getFieldsAndFragmentNames( $context, $fragmentType, @@ -777,63 +725,90 @@ class OverlappingFieldsCanBeMerged extends AbstractValidationRule } /** - * Given a reference to a fragment, return the represented collection of fields - * as well as a list of nested fragment names referenced via fragment spreads. + * Collect all conflicts found between two fragments, including via spreading in + * any nested fragments. * - * @param ValidationContext $context - * @param CompositeType $parentType - * @param SelectionSetNode $selectionSet - * @param array $astAndDefs - * @param array $fragmentNames + * @param mixed[][] $conflicts + * @param bool $areMutuallyExclusive + * @param string $fragmentName1 + * @param string $fragmentName2 */ - private function internalCollectFieldsAndFragmentNames( + private function collectConflictsBetweenFragments( ValidationContext $context, - $parentType, - SelectionSetNode $selectionSet, - array &$astAndDefs, - array &$fragmentNames - ) - { - $selectionSetLength = count($selectionSet->selections); - for ($i = 0; $i < $selectionSetLength; $i++) { - $selection = $selectionSet->selections[$i]; + array &$conflicts, + $areMutuallyExclusive, + $fragmentName1, + $fragmentName2 + ) { + // No need to compare a fragment to itself. + if ($fragmentName1 === $fragmentName2) { + return; + } - switch (true) { - case $selection instanceof FieldNode: - $fieldName = $selection->name->value; - $fieldDef = null; - if ($parentType instanceof ObjectType || - $parentType instanceof InterfaceType) { - $tmp = $parentType->getFields(); - if (isset($tmp[$fieldName])) { - $fieldDef = $tmp[$fieldName]; - } - } - $responseName = $selection->alias ? $selection->alias->value : $fieldName; + // Memoize so two fragments are not compared for conflicts more than once. + if ($this->comparedFragmentPairs->has( + $fragmentName1, + $fragmentName2, + $areMutuallyExclusive + ) + ) { + return; + } + $this->comparedFragmentPairs->add( + $fragmentName1, + $fragmentName2, + $areMutuallyExclusive + ); - if (!isset($astAndDefs[$responseName])) { - $astAndDefs[$responseName] = []; - } - $astAndDefs[$responseName][] = [$parentType, $selection, $fieldDef]; - break; - case $selection instanceof FragmentSpreadNode: - $fragmentNames[$selection->name->value] = true; - break; - case $selection instanceof InlineFragmentNode: - $typeCondition = $selection->typeCondition; - $inlineFragmentType = $typeCondition - ? TypeInfo::typeFromAST($context->getSchema(), $typeCondition) - : $parentType; + $fragment1 = $context->getFragment($fragmentName1); + $fragment2 = $context->getFragment($fragmentName2); + if (! $fragment1 || ! $fragment2) { + return; + } - $this->internalcollectFieldsAndFragmentNames( - $context, - $inlineFragmentType, - $selection->selectionSet, - $astAndDefs, - $fragmentNames - ); - break; - } + [$fieldMap1, $fragmentNames1] = $this->getReferencedFieldsAndFragmentNames( + $context, + $fragment1 + ); + [$fieldMap2, $fragmentNames2] = $this->getReferencedFieldsAndFragmentNames( + $context, + $fragment2 + ); + + // (F) First, collect all conflicts between these two collections of fields + // (not including any nested fragments). + $this->collectConflictsBetween( + $context, + $conflicts, + $areMutuallyExclusive, + $fieldMap1, + $fieldMap2 + ); + + // (G) Then collect conflicts between the first fragment and any nested + // fragments spread in the second fragment. + $fragmentNames2Length = count($fragmentNames2); + for ($j = 0; $j < $fragmentNames2Length; $j++) { + $this->collectConflictsBetweenFragments( + $context, + $conflicts, + $areMutuallyExclusive, + $fragmentName1, + $fragmentNames2[$j] + ); + } + + // (G) Then collect conflicts between the second fragment and any nested + // fragments spread in the first fragment. + $fragmentNames1Length = count($fragmentNames1); + for ($i = 0; $i < $fragmentNames1Length; $i++) { + $this->collectConflictsBetweenFragments( + $context, + $conflicts, + $areMutuallyExclusive, + $fragmentNames1[$i], + $fragmentName2 + ); } } @@ -841,42 +816,79 @@ class OverlappingFieldsCanBeMerged extends AbstractValidationRule * Given a series of Conflicts which occurred between two sub-fields, generate * a single Conflict. * - * @param array $conflicts - * @param string $responseName - * @param FieldNode $ast1 - * @param FieldNode $ast2 - * @return array|null + * @param mixed[][] $conflicts + * @param string $responseName + * @return mixed[]|null */ private function subfieldConflicts( array $conflicts, $responseName, FieldNode $ast1, FieldNode $ast2 - ) - { - if (count($conflicts) > 0) { - return [ - [ - $responseName, - array_map(function ($conflict) { - return $conflict[0]; - }, $conflicts), - ], - array_reduce( - $conflicts, - function ($allFields, $conflict) { - return array_merge($allFields, $conflict[1]); - }, - [$ast1] - ), - array_reduce( - $conflicts, - function ($allFields, $conflict) { - return array_merge($allFields, $conflict[2]); - }, - [$ast2] - ), - ]; + ) { + if (count($conflicts) === 0) { + return null; } + + return [ + [ + $responseName, + array_map( + function ($conflict) { + return $conflict[0]; + }, + $conflicts + ), + ], + array_reduce( + $conflicts, + function ($allFields, $conflict) { + return array_merge($allFields, $conflict[1]); + }, + [$ast1] + ), + array_reduce( + $conflicts, + function ($allFields, $conflict) { + return array_merge($allFields, $conflict[2]); + }, + [$ast2] + ), + ]; + } + + /** + * @param string $responseName + * @param string $reason + */ + public static function fieldsConflictMessage($responseName, $reason) + { + $reasonMessage = self::reasonMessage($reason); + + return sprintf( + 'Fields "%s" conflict because %s. Use different aliases on the fields to fetch both if this was intentional.', + $responseName, + $reasonMessage + ); + } + + public static function reasonMessage($reason) + { + if (is_array($reason)) { + $tmp = array_map( + function ($tmp) { + [$responseName, $subReason] = $tmp; + + $reasonMessage = self::reasonMessage($subReason); + + return sprintf('subfields "%s" conflict because %s', $responseName, $reasonMessage); + }, + $reason + ); + + return implode(' and ', $tmp); + } + + return $reason; } } diff --git a/src/Validator/Rules/PossibleFragmentSpreads.php b/src/Validator/Rules/PossibleFragmentSpreads.php index 0647e6f..26611e6 100644 --- a/src/Validator/Rules/PossibleFragmentSpreads.php +++ b/src/Validator/Rules/PossibleFragmentSpreads.php @@ -1,72 +1,61 @@ function(InlineFragmentNode $node) use ($context) { - $fragType = $context->getType(); + NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) use ($context) { + $fragType = $context->getType(); $parentType = $context->getParentType(); - if ($fragType instanceof CompositeType && - $parentType instanceof CompositeType && - !$this->doTypesOverlap($context->getSchema(), $fragType, $parentType)) { - $context->reportError(new Error( - self::typeIncompatibleAnonSpreadMessage($parentType, $fragType), - [$node] - )); + if (! ($fragType instanceof CompositeType) || + ! ($parentType instanceof CompositeType) || + $this->doTypesOverlap($context->getSchema(), $fragType, $parentType)) { + return; } + + $context->reportError(new Error( + self::typeIncompatibleAnonSpreadMessage($parentType, $fragType), + [$node] + )); }, - NodeKind::FRAGMENT_SPREAD => function(FragmentSpreadNode $node) use ($context) { - $fragName = $node->name->value; - $fragType = $this->getFragmentType($context, $fragName); + NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) use ($context) { + $fragName = $node->name->value; + $fragType = $this->getFragmentType($context, $fragName); $parentType = $context->getParentType(); - if ($fragType && $parentType && !$this->doTypesOverlap($context->getSchema(), $fragType, $parentType)) { - $context->reportError(new Error( - self::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType), - [$node] - )); + if (! $fragType || + ! $parentType || + $this->doTypesOverlap($context->getSchema(), $fragType, $parentType) + ) { + return; } - } - ]; - } - private function getFragmentType(ValidationContext $context, $name) - { - $frag = $context->getFragment($name); - if ($frag) { - $type = TypeInfo::typeFromAST($context->getSchema(), $frag->typeCondition); - if ($type instanceof CompositeType) { - return $type; - } - } - return null; + $context->reportError(new Error( + self::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType), + [$node] + )); + }, + ]; } private function doTypesOverlap(Schema $schema, CompositeType $fragType, CompositeType $parentType) @@ -136,4 +125,36 @@ class PossibleFragmentSpreads extends AbstractValidationRule return false; } + + public static function typeIncompatibleAnonSpreadMessage($parentType, $fragType) + { + return sprintf( + 'Fragment cannot be spread here as objects of type "%s" can never be of type "%s".', + $parentType, + $fragType + ); + } + + private function getFragmentType(ValidationContext $context, $name) + { + $frag = $context->getFragment($name); + if ($frag) { + $type = TypeInfo::typeFromAST($context->getSchema(), $frag->typeCondition); + if ($type instanceof CompositeType) { + return $type; + } + } + + return null; + } + + public static function typeIncompatibleSpreadMessage($fragName, $parentType, $fragType) + { + return sprintf( + 'Fragment "%s" cannot be spread here as objects of type "%s" can never be of type "%s".', + $fragName, + $parentType, + $fragType + ); + } } diff --git a/src/Validator/Rules/ProvidedNonNullArguments.php b/src/Validator/Rules/ProvidedNonNullArguments.php index 4d0c63c..56254af 100644 --- a/src/Validator/Rules/ProvidedNonNullArguments.php +++ b/src/Validator/Rules/ProvidedNonNullArguments.php @@ -1,4 +1,7 @@ [ - 'leave' => function(FieldNode $fieldNode) use ($context) { + NodeKind::FIELD => [ + 'leave' => function (FieldNode $fieldNode) use ($context) { $fieldDef = $context->getFieldDef(); - if (!$fieldDef) { + if (! $fieldDef) { return Visitor::skipNode(); } $argNodes = $fieldNode->arguments ?: []; @@ -38,39 +32,67 @@ class ProvidedNonNullArguments extends AbstractValidationRule $argNodeMap[$argNode->name->value] = $argNodes; } foreach ($fieldDef->args as $argDef) { - $argNode = isset($argNodeMap[$argDef->name]) ? $argNodeMap[$argDef->name] : null; - if (!$argNode && $argDef->getType() instanceof NonNull) { - $context->reportError(new Error( - self::missingFieldArgMessage($fieldNode->name->value, $argDef->name, $argDef->getType()), - [$fieldNode] - )); + $argNode = $argNodeMap[$argDef->name] ?? null; + if ($argNode || ! ($argDef->getType() instanceof NonNull)) { + continue; } + + $context->reportError(new Error( + self::missingFieldArgMessage($fieldNode->name->value, $argDef->name, $argDef->getType()), + [$fieldNode] + )); } - } + }, ], NodeKind::DIRECTIVE => [ - 'leave' => function(DirectiveNode $directiveNode) use ($context) { + 'leave' => function (DirectiveNode $directiveNode) use ($context) { $directiveDef = $context->getDirective(); - if (!$directiveDef) { + if (! $directiveDef) { return Visitor::skipNode(); } - $argNodes = $directiveNode->arguments ?: []; + $argNodes = $directiveNode->arguments ?: []; $argNodeMap = []; foreach ($argNodes as $argNode) { $argNodeMap[$argNode->name->value] = $argNodes; } foreach ($directiveDef->args as $argDef) { - $argNode = isset($argNodeMap[$argDef->name]) ? $argNodeMap[$argDef->name] : null; - if (!$argNode && $argDef->getType() instanceof NonNull) { - $context->reportError(new Error( - self::missingDirectiveArgMessage($directiveNode->name->value, $argDef->name, $argDef->getType()), - [$directiveNode] - )); + $argNode = $argNodeMap[$argDef->name] ?? null; + if ($argNode || ! ($argDef->getType() instanceof NonNull)) { + continue; } + + $context->reportError(new Error( + self::missingDirectiveArgMessage( + $directiveNode->name->value, + $argDef->name, + $argDef->getType() + ), + [$directiveNode] + )); } - } - ] + }, + ], ]; } + + public static function missingFieldArgMessage($fieldName, $argName, $type) + { + return sprintf( + 'Field "%s" argument "%s" of type "%s" is required but not provided.', + $fieldName, + $argName, + $type + ); + } + + public static function missingDirectiveArgMessage($directiveName, $argName, $type) + { + return sprintf( + 'Directive "@%s" argument "%s" of type "%s" is required but not provided.', + $directiveName, + $argName, + $type + ); + } } diff --git a/src/Validator/Rules/QueryComplexity.php b/src/Validator/Rules/QueryComplexity.php index 638010d..f126238 100644 --- a/src/Validator/Rules/QueryComplexity.php +++ b/src/Validator/Rules/QueryComplexity.php @@ -1,4 +1,7 @@ setMaxQueryComplexity($maxQueryComplexity); } - public static function maxQueryComplexityErrorMessage($max, $count) - { - return sprintf('Max query complexity should be %d but got %d.', $max, $count); - } - - /** - * Set max query complexity. If equal to 0 no check is done. Must be greater or equal to 0. - * - * @param $maxQueryComplexity - */ - public function setMaxQueryComplexity($maxQueryComplexity) - { - $this->checkIfGreaterOrEqualToZero('maxQueryComplexity', $maxQueryComplexity); - - $this->maxQueryComplexity = (int) $maxQueryComplexity; - } - - public function getMaxQueryComplexity() - { - return $this->maxQueryComplexity; - } - - public function setRawVariableValues(array $rawVariableValues = null) - { - $this->rawVariableValues = $rawVariableValues ?: []; - } - - public function getRawVariableValues() - { - return $this->rawVariableValues; - } - public function getVisitor(ValidationContext $context) { $this->context = $context; - $this->variableDefs = new \ArrayObject(); + $this->variableDefs = new \ArrayObject(); $this->fieldNodeAndDefs = new \ArrayObject(); - $complexity = 0; + $complexity = 0; return $this->invokeIfNeeded( $context, [ - NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context) { + NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context) { $this->fieldNodeAndDefs = $this->collectFieldASTsAndDefs( $context, $context->getParentType(), @@ -87,23 +65,31 @@ class QueryComplexity extends AbstractQuerySecurity $this->fieldNodeAndDefs ); }, - NodeKind::VARIABLE_DEFINITION => function ($def) { + NodeKind::VARIABLE_DEFINITION => function ($def) { $this->variableDefs[] = $def; + return Visitor::skipNode(); }, NodeKind::OPERATION_DEFINITION => [ 'leave' => function (OperationDefinitionNode $operationDefinition) use ($context, &$complexity) { $errors = $context->getErrors(); - if (empty($errors)) { - $complexity = $this->fieldComplexity($operationDefinition, $complexity); - - if ($complexity > $this->getMaxQueryComplexity()) { - $context->reportError( - new Error($this->maxQueryComplexityErrorMessage($this->getMaxQueryComplexity(), $complexity)) - ); - } + if (! empty($errors)) { + return; } + + $complexity = $this->fieldComplexity($operationDefinition, $complexity); + + if ($complexity <= $this->getMaxQueryComplexity()) { + return; + } + + $context->reportError( + new Error($this->maxQueryComplexityErrorMessage( + $this->getMaxQueryComplexity(), + $complexity + )) + ); }, ], ] @@ -125,9 +111,9 @@ class QueryComplexity extends AbstractQuerySecurity { switch ($node->kind) { case NodeKind::FIELD: - /* @var FieldNode $node */ + /** @var FieldNode $node */ // default values - $args = []; + $args = []; $complexityFn = FieldDefinition::DEFAULT_COMPLEXITY_FN; // calculate children complexity if needed @@ -139,7 +125,7 @@ class QueryComplexity extends AbstractQuerySecurity } $astFieldInfo = $this->astFieldInfo($node); - $fieldDef = $astFieldInfo[1]; + $fieldDef = $astFieldInfo[1]; if ($fieldDef instanceof FieldDefinition) { if ($this->directiveExcludesField($node)) { @@ -157,7 +143,7 @@ class QueryComplexity extends AbstractQuerySecurity break; case NodeKind::INLINE_FRAGMENT: - /* @var InlineFragmentNode $node */ + /** @var InlineFragmentNode $node */ // node has children? if (isset($node->selectionSet)) { $complexity = $this->fieldComplexity($node, $complexity); @@ -165,10 +151,10 @@ class QueryComplexity extends AbstractQuerySecurity break; case NodeKind::FRAGMENT_SPREAD: - /* @var FragmentSpreadNode $node */ + /** @var FragmentSpreadNode $node */ $fragment = $this->getFragment($node); - if (null !== $fragment) { + if ($fragment !== null) { $complexity = $this->fieldComplexity($fragment, $complexity); } break; @@ -179,11 +165,11 @@ class QueryComplexity extends AbstractQuerySecurity private function astFieldInfo(FieldNode $field) { - $fieldName = $this->getFieldName($field); + $fieldName = $this->getFieldName($field); $astFieldInfo = [null, null]; if (isset($this->fieldNodeAndDefs[$fieldName])) { foreach ($this->fieldNodeAndDefs[$fieldName] as $astAndDef) { - if ($astAndDef[0] == $field) { + if ($astAndDef[0] === $field) { $astFieldInfo = $astAndDef; break; } @@ -193,37 +179,8 @@ class QueryComplexity extends AbstractQuerySecurity return $astFieldInfo; } - private function buildFieldArguments(FieldNode $node) + private function directiveExcludesField(FieldNode $node) { - $rawVariableValues = $this->getRawVariableValues(); - $astFieldInfo = $this->astFieldInfo($node); - $fieldDef = $astFieldInfo[1]; - - $args = []; - - if ($fieldDef instanceof FieldDefinition) { - $variableValuesResult = Values::getVariableValues( - $this->context->getSchema(), - $this->variableDefs, - $rawVariableValues - ); - - if ($variableValuesResult['errors']) { - throw new Error(implode("\n\n", array_map( - function ($error) { - return $error->getMessage(); - } - , $variableValuesResult['errors']))); - } - $variableValues = $variableValuesResult['coerced']; - - $args = Values::getArgumentValues($fieldDef, $node, $variableValues); - } - - return $args; - } - - private function directiveExcludesField(FieldNode $node) { foreach ($node->directives as $directiveNode) { if ($directiveNode->name->value === 'deprecated') { return false; @@ -236,28 +193,99 @@ class QueryComplexity extends AbstractQuerySecurity ); if ($variableValuesResult['errors']) { - throw new Error(implode("\n\n", array_map( - function ($error) { - return $error->getMessage(); - } - , $variableValuesResult['errors']))); + throw new Error(implode( + "\n\n", + array_map( + function ($error) { + return $error->getMessage(); + }, + $variableValuesResult['errors'] + ) + )); } $variableValues = $variableValuesResult['coerced']; if ($directiveNode->name->value === 'include') { - $directive = Directive::includeDirective(); + $directive = Directive::includeDirective(); $directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues); - return !$directiveArgs['if']; - } else { - $directive = Directive::skipDirective(); - $directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues); - - return $directiveArgs['if']; + return ! $directiveArgs['if']; } + + $directive = Directive::skipDirective(); + $directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues); + + return $directiveArgs['if']; } } + public function getRawVariableValues() + { + return $this->rawVariableValues; + } + + /** + * @param mixed[]|null $rawVariableValues + */ + public function setRawVariableValues(?array $rawVariableValues = null) + { + $this->rawVariableValues = $rawVariableValues ?: []; + } + + private function buildFieldArguments(FieldNode $node) + { + $rawVariableValues = $this->getRawVariableValues(); + $astFieldInfo = $this->astFieldInfo($node); + $fieldDef = $astFieldInfo[1]; + + $args = []; + + if ($fieldDef instanceof FieldDefinition) { + $variableValuesResult = Values::getVariableValues( + $this->context->getSchema(), + $this->variableDefs, + $rawVariableValues + ); + + if ($variableValuesResult['errors']) { + throw new Error(implode( + "\n\n", + array_map( + function ($error) { + return $error->getMessage(); + }, + $variableValuesResult['errors'] + ) + )); + } + $variableValues = $variableValuesResult['coerced']; + + $args = Values::getArgumentValues($fieldDef, $node, $variableValues); + } + + return $args; + } + + public function getMaxQueryComplexity() + { + return $this->maxQueryComplexity; + } + + /** + * Set max query complexity. If equal to 0 no check is done. Must be greater or equal to 0. + */ + public function setMaxQueryComplexity($maxQueryComplexity) + { + $this->checkIfGreaterOrEqualToZero('maxQueryComplexity', $maxQueryComplexity); + + $this->maxQueryComplexity = (int) $maxQueryComplexity; + } + + public static function maxQueryComplexityErrorMessage($max, $count) + { + return sprintf('Max query complexity should be %d but got %d.', $max, $count); + } + protected function isEnabled() { return $this->getMaxQueryComplexity() !== static::DISABLED; diff --git a/src/Validator/Rules/QueryDepth.php b/src/Validator/Rules/QueryDepth.php index 5fb7065..1408eee 100644 --- a/src/Validator/Rules/QueryDepth.php +++ b/src/Validator/Rules/QueryDepth.php @@ -1,21 +1,20 @@ setMaxQueryDepth($maxQueryDepth); } - /** - * Set max query depth. If equal to 0 no check is done. Must be greater or equal to 0. - * - * @param $maxQueryDepth - */ - public function setMaxQueryDepth($maxQueryDepth) - { - $this->checkIfGreaterOrEqualToZero('maxQueryDepth', $maxQueryDepth); - - $this->maxQueryDepth = (int) $maxQueryDepth; - } - - public function getMaxQueryDepth() - { - return $this->maxQueryDepth; - } - - public static function maxQueryDepthErrorMessage($max, $count) - { - return sprintf('Max query depth should be %d but got %d.', $max, $count); - } - public function getVisitor(ValidationContext $context) { return $this->invokeIfNeeded( @@ -54,22 +31,19 @@ class QueryDepth extends AbstractQuerySecurity 'leave' => function (OperationDefinitionNode $operationDefinition) use ($context) { $maxDepth = $this->fieldDepth($operationDefinition); - if ($maxDepth > $this->getMaxQueryDepth()) { - $context->reportError( - new Error($this->maxQueryDepthErrorMessage($this->getMaxQueryDepth(), $maxDepth)) - ); + if ($maxDepth <= $this->getMaxQueryDepth()) { + return; } + + $context->reportError( + new Error($this->maxQueryDepthErrorMessage($this->getMaxQueryDepth(), $maxDepth)) + ); }, ], ] ); } - protected function isEnabled() - { - return $this->getMaxQueryDepth() !== static::DISABLED; - } - private function fieldDepth($node, $depth = 0, $maxDepth = 0) { if (isset($node->selectionSet) && $node->selectionSet instanceof SelectionSetNode) { @@ -85,9 +59,9 @@ class QueryDepth extends AbstractQuerySecurity { switch ($node->kind) { case NodeKind::FIELD: - /* @var FieldNode $node */ + /** @var FieldNode $node */ // node has children? - if (null !== $node->selectionSet) { + if ($node->selectionSet !== null) { // update maxDepth if needed if ($depth > $maxDepth) { $maxDepth = $depth; @@ -97,18 +71,18 @@ class QueryDepth extends AbstractQuerySecurity break; case NodeKind::INLINE_FRAGMENT: - /* @var InlineFragmentNode $node */ + /** @var InlineFragmentNode $node */ // node has children? - if (null !== $node->selectionSet) { + if ($node->selectionSet !== null) { $maxDepth = $this->fieldDepth($node, $depth, $maxDepth); } break; case NodeKind::FRAGMENT_SPREAD: - /* @var FragmentSpreadNode $node */ + /** @var FragmentSpreadNode $node */ $fragment = $this->getFragment($node); - if (null !== $fragment) { + if ($fragment !== null) { $maxDepth = $this->fieldDepth($fragment, $depth, $maxDepth); } break; @@ -116,4 +90,31 @@ class QueryDepth extends AbstractQuerySecurity return $maxDepth; } + + public function getMaxQueryDepth() + { + return $this->maxQueryDepth; + } + + /** + * Set max query depth. If equal to 0 no check is done. Must be greater or equal to 0. + * + * @param int $maxQueryDepth + */ + public function setMaxQueryDepth($maxQueryDepth) + { + $this->checkIfGreaterOrEqualToZero('maxQueryDepth', $maxQueryDepth); + + $this->maxQueryDepth = (int) $maxQueryDepth; + } + + public static function maxQueryDepthErrorMessage($max, $count) + { + return sprintf('Max query depth should be %d but got %d.', $max, $count); + } + + protected function isEnabled() + { + return $this->getMaxQueryDepth() !== static::DISABLED; + } } diff --git a/src/Validator/Rules/AbstractQuerySecurity.php b/src/Validator/Rules/QuerySecurityRule.php similarity index 72% rename from src/Validator/Rules/AbstractQuerySecurity.php rename to src/Validator/Rules/QuerySecurityRule.php index cfdc7bc..ab97fed 100644 --- a/src/Validator/Rules/AbstractQuerySecurity.php +++ b/src/Validator/Rules/QuerySecurityRule.php @@ -1,40 +1,35 @@ fragments; - } - /** * check if equal to 0 no check is done. Must be greater or equal to 0. * - * @param $value + * @param string $name + * @param int $value */ protected function checkIfGreaterOrEqualToZero($name, $value) { @@ -43,30 +38,30 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule } } - protected function gatherFragmentDefinition(ValidationContext $context) - { - // Gather all the fragment definition. - // Importantly this does not include inline fragments. - $definitions = $context->getDocument()->definitions; - foreach ($definitions as $node) { - if ($node instanceof FragmentDefinitionNode) { - $this->fragments[$node->name->value] = $node; - } - } - } - protected function getFragment(FragmentSpreadNode $fragmentSpread) { $spreadName = $fragmentSpread->name->value; - $fragments = $this->getFragments(); + $fragments = $this->getFragments(); - return isset($fragments[$spreadName]) ? $fragments[$spreadName] : null; + return $fragments[$spreadName] ?? null; } + /** + * @return FragmentDefinitionNode[] + */ + protected function getFragments() + { + return $this->fragments; + } + + /** + * @param Closure[] $validators + * @return Closure[] + */ protected function invokeIfNeeded(ValidationContext $context, array $validators) { // is disabled? - if (!$this->isEnabled()) { + if (! $this->isEnabled()) { return []; } @@ -75,6 +70,22 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule return $validators; } + abstract protected function isEnabled(); + + protected function gatherFragmentDefinition(ValidationContext $context) + { + // Gather all the fragment definition. + // Importantly this does not include inline fragments. + $definitions = $context->getDocument()->definitions; + foreach ($definitions as $node) { + if (! ($node instanceof FragmentDefinitionNode)) { + continue; + } + + $this->fragments[$node->name->value] = $node; + } + } + /** * Given a selectionSet, adds all of the fields in that selection to * the passed in map of fields, and returns it at the end. @@ -85,29 +96,30 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule * * @see \GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged * - * @param ValidationContext $context - * @param Type|null $parentType - * @param SelectionSetNode $selectionSet - * @param \ArrayObject $visitedFragmentNames - * @param \ArrayObject $astAndDefs + * @param Type|null $parentType * * @return \ArrayObject */ - protected function collectFieldASTsAndDefs(ValidationContext $context, $parentType, SelectionSetNode $selectionSet, \ArrayObject $visitedFragmentNames = null, \ArrayObject $astAndDefs = null) - { + protected function collectFieldASTsAndDefs( + ValidationContext $context, + $parentType, + SelectionSetNode $selectionSet, + ?\ArrayObject $visitedFragmentNames = null, + ?\ArrayObject $astAndDefs = null + ) { $_visitedFragmentNames = $visitedFragmentNames ?: new \ArrayObject(); - $_astAndDefs = $astAndDefs ?: new \ArrayObject(); + $_astAndDefs = $astAndDefs ?: new \ArrayObject(); foreach ($selectionSet->selections as $selection) { - switch ($selection->kind) { + switch ($selection->getKind()) { case NodeKind::FIELD: - /* @var FieldNode $selection */ + /** @var FieldNode $selection */ $fieldName = $selection->name->value; - $fieldDef = null; + $fieldDef = null; if ($parentType && method_exists($parentType, 'getFields')) { - $tmp = $parentType->getFields(); - $schemaMetaFieldDef = Introspection::schemaMetaFieldDef(); - $typeMetaFieldDef = Introspection::typeMetaFieldDef(); + $tmp = $parentType->getFields(); + $schemaMetaFieldDef = Introspection::schemaMetaFieldDef(); + $typeMetaFieldDef = Introspection::typeMetaFieldDef(); $typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef(); if ($fieldName === $schemaMetaFieldDef->name && $context->getSchema()->getQueryType() === $parentType) { @@ -121,14 +133,14 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule } } $responseName = $this->getFieldName($selection); - if (!isset($_astAndDefs[$responseName])) { + if (! isset($_astAndDefs[$responseName])) { $_astAndDefs[$responseName] = new \ArrayObject(); } // create field context $_astAndDefs[$responseName][] = [$selection, $fieldDef]; break; case NodeKind::INLINE_FRAGMENT: - /* @var InlineFragmentNode $selection */ + /** @var InlineFragmentNode $selection */ $_astAndDefs = $this->collectFieldASTsAndDefs( $context, TypeInfo::typeFromAST($context->getSchema(), $selection->typeCondition), @@ -138,12 +150,12 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule ); break; case NodeKind::FRAGMENT_SPREAD: - /* @var FragmentSpreadNode $selection */ + /** @var FragmentSpreadNode $selection */ $fragName = $selection->name->value; if (empty($_visitedFragmentNames[$fragName])) { $_visitedFragmentNames[$fragName] = true; - $fragment = $context->getFragment($fragName); + $fragment = $context->getFragment($fragName); if ($fragment) { $_astAndDefs = $this->collectFieldASTsAndDefs( @@ -165,10 +177,9 @@ abstract class AbstractQuerySecurity extends AbstractValidationRule protected function getFieldName(FieldNode $node) { $fieldName = $node->name->value; - $responseName = $node->alias ? $node->alias->value : $fieldName; - return $responseName; + return $node->alias ? $node->alias->value : $fieldName; } - - abstract protected function isEnabled(); } + +class_alias(QuerySecurityRule::class, 'GraphQL\Validator\Rules\AbstractQuerySecurity'); diff --git a/src/Validator/Rules/ScalarLeafs.php b/src/Validator/Rules/ScalarLeafs.php index 670403d..a96b5d8 100644 --- a/src/Validator/Rules/ScalarLeafs.php +++ b/src/Validator/Rules/ScalarLeafs.php @@ -1,4 +1,7 @@ function(FieldNode $node) use ($context) { + NodeKind::FIELD => function (FieldNode $node) use ($context) { $type = $context->getType(); - if ($type) { - if (Type::isLeafType(Type::getNamedType($type))) { - if ($node->selectionSet) { - $context->reportError(new Error( - self::noSubselectionAllowedMessage($node->name->value, $type), - [$node->selectionSet] - )); - } - } else if (!$node->selectionSet) { + if (! $type) { + return; + } + + if (Type::isLeafType(Type::getNamedType($type))) { + if ($node->selectionSet) { $context->reportError(new Error( - self::requiredSubselectionMessage($node->name->value, $type), - [$node] + self::noSubselectionAllowedMessage($node->name->value, $type), + [$node->selectionSet] )); } + } elseif (! $node->selectionSet) { + $context->reportError(new Error( + self::requiredSubselectionMessage($node->name->value, $type), + [$node] + )); } - } + }, ]; } + + public static function noSubselectionAllowedMessage($field, $type) + { + return sprintf('Field "%s" of type "%s" must not have a sub selection.', $field, $type); + } + + public static function requiredSubselectionMessage($field, $type) + { + return sprintf('Field "%s" of type "%s" must have a sub selection.', $field, $type); + } } diff --git a/src/Validator/Rules/UniqueArgumentNames.php b/src/Validator/Rules/UniqueArgumentNames.php index 7c6eef1..2e83d43 100644 --- a/src/Validator/Rules/UniqueArgumentNames.php +++ b/src/Validator/Rules/UniqueArgumentNames.php @@ -1,20 +1,20 @@ knownArgNames = []; return [ - NodeKind::FIELD => function () { - $this->knownArgNames = [];; + NodeKind::FIELD => function () { + $this->knownArgNames = []; }, NodeKind::DIRECTIVE => function () { $this->knownArgNames = []; }, - NodeKind::ARGUMENT => function (ArgumentNode $node) use ($context) { + NodeKind::ARGUMENT => function (ArgumentNode $node) use ($context) { $argName = $node->name->value; - if (!empty($this->knownArgNames[$argName])) { + if (! empty($this->knownArgNames[$argName])) { $context->reportError(new Error( self::duplicateArgMessage($argName), [$this->knownArgNames[$argName], $node->name] @@ -38,8 +38,14 @@ class UniqueArgumentNames extends AbstractValidationRule } else { $this->knownArgNames[$argName] = $node->name; } + return Visitor::skipNode(); - } + }, ]; } + + public static function duplicateArgMessage($argName) + { + return sprintf('There can be only one argument named "%s".', $argName); + } } diff --git a/src/Validator/Rules/UniqueDirectivesPerLocation.php b/src/Validator/Rules/UniqueDirectivesPerLocation.php index 08d778a..c049c82 100644 --- a/src/Validator/Rules/UniqueDirectivesPerLocation.php +++ b/src/Validator/Rules/UniqueDirectivesPerLocation.php @@ -1,38 +1,44 @@ function(Node $node) use ($context) { - if (isset($node->directives)) { - $knownDirectives = []; - foreach ($node->directives as $directive) { - /** @var DirectiveNode $directive */ - $directiveName = $directive->name->value; - if (isset($knownDirectives[$directiveName])) { - $context->reportError(new Error( - self::duplicateDirectiveMessage($directiveName), - [$knownDirectives[$directiveName], $directive] - )); - } else { - $knownDirectives[$directiveName] = $directive; - } + 'enter' => function (Node $node) use ($context) { + if (! isset($node->directives)) { + return; + } + + $knownDirectives = []; + foreach ($node->directives as $directive) { + /** @var DirectiveNode $directive */ + $directiveName = $directive->name->value; + if (isset($knownDirectives[$directiveName])) { + $context->reportError(new Error( + self::duplicateDirectiveMessage($directiveName), + [$knownDirectives[$directiveName], $directive] + )); + } else { + $knownDirectives[$directiveName] = $directive; } } - } + }, ]; } + + public static function duplicateDirectiveMessage($directiveName) + { + return sprintf('The directive "%s" can only be used once at this location.', $directiveName); + } } diff --git a/src/Validator/Rules/UniqueFragmentNames.php b/src/Validator/Rules/UniqueFragmentNames.php index ebeb0b2..e29de7b 100644 --- a/src/Validator/Rules/UniqueFragmentNames.php +++ b/src/Validator/Rules/UniqueFragmentNames.php @@ -1,19 +1,20 @@ function () { return Visitor::skipNode(); }, - NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) { + NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) { $fragmentName = $node->name->value; - if (!empty($this->knownFragmentNames[$fragmentName])) { + if (empty($this->knownFragmentNames[$fragmentName])) { + $this->knownFragmentNames[$fragmentName] = $node->name; + } else { $context->reportError(new Error( self::duplicateFragmentNameMessage($fragmentName), - [ $this->knownFragmentNames[$fragmentName], $node->name ] + [$this->knownFragmentNames[$fragmentName], $node->name] )); - } else { - $this->knownFragmentNames[$fragmentName] = $node->name; } + return Visitor::skipNode(); - } + }, ]; } + + public static function duplicateFragmentNameMessage($fragName) + { + return sprintf('There can be only one fragment named "%s".', $fragName); + } } diff --git a/src/Validator/Rules/UniqueInputFieldNames.php b/src/Validator/Rules/UniqueInputFieldNames.php index 1e48d48..6426b43 100644 --- a/src/Validator/Rules/UniqueInputFieldNames.php +++ b/src/Validator/Rules/UniqueInputFieldNames.php @@ -1,4 +1,7 @@ knownNames = []; + $this->knownNames = []; $this->knownNameStack = []; return [ - NodeKind::OBJECT => [ - 'enter' => function() { + NodeKind::OBJECT => [ + 'enter' => function () { $this->knownNameStack[] = $this->knownNames; - $this->knownNames = []; + $this->knownNames = []; }, - 'leave' => function() { + 'leave' => function () { $this->knownNames = array_pop($this->knownNameStack); - } + }, ], - NodeKind::OBJECT_FIELD => function(ObjectFieldNode $node) use ($context) { + NodeKind::OBJECT_FIELD => function (ObjectFieldNode $node) use ($context) { $fieldName = $node->name->value; - if (!empty($this->knownNames[$fieldName])) { + if (! empty($this->knownNames[$fieldName])) { $context->reportError(new Error( self::duplicateInputFieldMessage($fieldName), - [ $this->knownNames[$fieldName], $node->name ] + [$this->knownNames[$fieldName], $node->name] )); } else { $this->knownNames[$fieldName] = $node->name; } + return Visitor::skipNode(); - } + }, ]; } + + public static function duplicateInputFieldMessage($fieldName) + { + return sprintf('There can be only one input field named "%s".', $fieldName); + } } diff --git a/src/Validator/Rules/UniqueOperationNames.php b/src/Validator/Rules/UniqueOperationNames.php index 80a352c..232380d 100644 --- a/src/Validator/Rules/UniqueOperationNames.php +++ b/src/Validator/Rules/UniqueOperationNames.php @@ -1,19 +1,20 @@ knownOperationNames = []; return [ - NodeKind::OPERATION_DEFINITION => function(OperationDefinitionNode $node) use ($context) { + NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) use ($context) { $operationName = $node->name; if ($operationName) { - if (!empty($this->knownOperationNames[$operationName->value])) { + if (empty($this->knownOperationNames[$operationName->value])) { + $this->knownOperationNames[$operationName->value] = $operationName; + } else { $context->reportError(new Error( self::duplicateOperationNameMessage($operationName->value), - [ $this->knownOperationNames[$operationName->value], $operationName ] + [$this->knownOperationNames[$operationName->value], $operationName] )); - } else { - $this->knownOperationNames[$operationName->value] = $operationName; } } + return Visitor::skipNode(); }, - NodeKind::FRAGMENT_DEFINITION => function() { + NodeKind::FRAGMENT_DEFINITION => function () { return Visitor::skipNode(); - } + }, ]; } + + public static function duplicateOperationNameMessage($operationName) + { + return sprintf('There can be only one operation named "%s".', $operationName); + } } diff --git a/src/Validator/Rules/UniqueVariableNames.php b/src/Validator/Rules/UniqueVariableNames.php index 4329721..f050a73 100644 --- a/src/Validator/Rules/UniqueVariableNames.php +++ b/src/Validator/Rules/UniqueVariableNames.php @@ -1,18 +1,19 @@ knownVariableNames = []; return [ - NodeKind::OPERATION_DEFINITION => function() { + NodeKind::OPERATION_DEFINITION => function () { $this->knownVariableNames = []; }, - NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $node) use ($context) { + NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) use ($context) { $variableName = $node->variable->name->value; - if (!empty($this->knownVariableNames[$variableName])) { + if (empty($this->knownVariableNames[$variableName])) { + $this->knownVariableNames[$variableName] = $node->variable->name; + } else { $context->reportError(new Error( self::duplicateVariableMessage($variableName), - [ $this->knownVariableNames[$variableName], $node->variable->name ] + [$this->knownVariableNames[$variableName], $node->variable->name] )); - } else { - $this->knownVariableNames[$variableName] = $node->variable->name; } - } + }, ]; } + + public static function duplicateVariableMessage($variableName) + { + return sprintf('There can be only one variable named "%s".', $variableName); + } } diff --git a/src/Validator/Rules/AbstractValidationRule.php b/src/Validator/Rules/ValidationRule.php similarity index 67% rename from src/Validator/Rules/AbstractValidationRule.php rename to src/Validator/Rules/ValidationRule.php index 0926c29..c35388f 100644 --- a/src/Validator/Rules/AbstractValidationRule.php +++ b/src/Validator/Rules/ValidationRule.php @@ -1,10 +1,16 @@ function(NullValueNode $node) use ($context) { + NodeKind::NULL => function (NullValueNode $node) use ($context) { $type = $context->getInputType(); - if ($type instanceof NonNull) { - $context->reportError( - new Error( - self::badValueMessage((string) $type, Printer::doPrint($node)), - $node - ) - ); + if (! ($type instanceof NonNull)) { + return; } + + $context->reportError( + new Error( + self::badValueMessage((string) $type, Printer::doPrint($node)), + $node + ) + ); }, - NodeKind::LST => function(ListValueNode $node) use ($context) { + NodeKind::LST => function (ListValueNode $node) use ($context) { // Note: TypeInfo will traverse into a list's item type, so look to the // parent input type to check if it is a list. $type = Type::getNullableType($context->getParentInputType()); - if (!$type instanceof ListOfType) { + if (! $type instanceof ListOfType) { $this->isValidScalar($context, $node); + return Visitor::skipNode(); } }, - NodeKind::OBJECT => function(ObjectValueNode $node) use ($context) { + NodeKind::OBJECT => function (ObjectValueNode $node) use ($context) { // Note: TypeInfo will traverse into a list's item type, so look to the // parent input type to check if it is a list. $type = Type::getNamedType($context->getInputType()); - if (!$type instanceof InputObjectType) { + if (! $type instanceof InputObjectType) { $this->isValidScalar($context, $node); + return Visitor::skipNode(); } // Ensure every required field exists. - $inputFields = $type->getFields(); - $nodeFields = iterator_to_array($node->fields); + $inputFields = $type->getFields(); + $nodeFields = iterator_to_array($node->fields); $fieldNodeMap = array_combine( - array_map(function ($field) { return $field->name->value; }, $nodeFields), + array_map( + function ($field) { + return $field->name->value; + }, + $nodeFields + ), array_values($nodeFields) ); foreach ($inputFields as $fieldName => $fieldDef) { $fieldType = $fieldDef->getType(); - if (!isset($fieldNodeMap[$fieldName]) && $fieldType instanceof NonNull) { - $context->reportError( - new Error( - self::requiredFieldMessage($type->name, $fieldName, (string) $fieldType), - $node - ) - ); + if (isset($fieldNodeMap[$fieldName]) || ! ($fieldType instanceof NonNull)) { + continue; } - } - }, - NodeKind::OBJECT_FIELD => function(ObjectFieldNode $node) use ($context) { - $parentType = Type::getNamedType($context->getParentInputType()); - $fieldType = $context->getInputType(); - if (!$fieldType && $parentType instanceof InputObjectType) { - $suggestions = Utils::suggestionList( - $node->name->value, - array_keys($parentType->getFields()) - ); - $didYouMean = $suggestions - ? "Did you mean " . Utils::orList($suggestions) . "?" - : null; $context->reportError( new Error( - self::unknownFieldMessage($parentType->name, $node->name->value, $didYouMean), + self::requiredFieldMessage($type->name, $fieldName, (string) $fieldType), $node ) ); } }, - NodeKind::ENUM => function(EnumValueNode $node) use ($context) { + NodeKind::OBJECT_FIELD => function (ObjectFieldNode $node) use ($context) { + $parentType = Type::getNamedType($context->getParentInputType()); + $fieldType = $context->getInputType(); + if ($fieldType || ! ($parentType instanceof InputObjectType)) { + return; + } + + $suggestions = Utils::suggestionList( + $node->name->value, + array_keys($parentType->getFields()) + ); + $didYouMean = $suggestions + ? 'Did you mean ' . Utils::orList($suggestions) . '?' + : null; + + $context->reportError( + new Error( + self::unknownFieldMessage($parentType->name, $node->name->value, $didYouMean), + $node + ) + ); + }, + NodeKind::ENUM => function (EnumValueNode $node) use ($context) { $type = Type::getNamedType($context->getInputType()); - if (!$type instanceof EnumType) { + if (! $type instanceof EnumType) { $this->isValidScalar($context, $node); - } else if (!$type->getValue($node->value)) { + } elseif (! $type->getValue($node->value)) { $context->reportError( new Error( self::badValueMessage( @@ -140,25 +142,39 @@ class ValuesOfCorrectType extends AbstractValidationRule ); } }, - NodeKind::INT => function (IntValueNode $node) use ($context) { $this->isValidScalar($context, $node); }, - NodeKind::FLOAT => function (FloatValueNode $node) use ($context) { $this->isValidScalar($context, $node); }, - NodeKind::STRING => function (StringValueNode $node) use ($context) { $this->isValidScalar($context, $node); }, - NodeKind::BOOLEAN => function (BooleanValueNode $node) use ($context) { $this->isValidScalar($context, $node); }, + NodeKind::INT => function (IntValueNode $node) use ($context) { + $this->isValidScalar($context, $node); + }, + NodeKind::FLOAT => function (FloatValueNode $node) use ($context) { + $this->isValidScalar($context, $node); + }, + NodeKind::STRING => function (StringValueNode $node) use ($context) { + $this->isValidScalar($context, $node); + }, + NodeKind::BOOLEAN => function (BooleanValueNode $node) use ($context) { + $this->isValidScalar($context, $node); + }, ]; } + public static function badValueMessage($typeName, $valueName, $message = null) + { + return sprintf('Expected type %s, found %s', $typeName, $valueName) . + ($message ? "; ${message}" : '.'); + } + private function isValidScalar(ValidationContext $context, ValueNode $node) { // Report any error at the full type expected by the location. $locationType = $context->getInputType(); - if (!$locationType) { + if (! $locationType) { return; } $type = Type::getNamedType($locationType); - if (!$type instanceof ScalarType) { + if (! $type instanceof ScalarType) { $context->reportError( new Error( self::badValueMessage( @@ -169,6 +185,7 @@ class ValuesOfCorrectType extends AbstractValidationRule $node ) ); + return; } @@ -216,12 +233,26 @@ class ValuesOfCorrectType extends AbstractValidationRule if ($type instanceof EnumType) { $suggestions = Utils::suggestionList( Printer::doPrint($node), - array_map(function (EnumValueDefinition $value) { - return $value->name; - }, $type->getValues()) + array_map( + function (EnumValueDefinition $value) { + return $value->name; + }, + $type->getValues() + ) ); return $suggestions ? 'Did you mean the enum value ' . Utils::orList($suggestions) . '?' : null; } } + + public static function requiredFieldMessage($typeName, $fieldName, $fieldTypeName) + { + return sprintf('Field %s.%s of required type %s was not provided.', $typeName, $fieldName, $fieldTypeName); + } + + public static function unknownFieldMessage($typeName, $fieldName, $message = null) + { + return sprintf('Field "%s" is not defined by type %s', $fieldName, $typeName) . + ($message ? sprintf('; %s', $message) : '.'); + } } diff --git a/src/Validator/Rules/VariablesAreInputTypes.php b/src/Validator/Rules/VariablesAreInputTypes.php index a8f1bbf..4abc2e5 100644 --- a/src/Validator/Rules/VariablesAreInputTypes.php +++ b/src/Validator/Rules/VariablesAreInputTypes.php @@ -1,39 +1,42 @@ function(VariableDefinitionNode $node) use ($context) { + NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) use ($context) { $type = TypeInfo::typeFromAST($context->getSchema(), $node->type); // If the variable type is not an input type, return an error. - if ($type && !Type::isInputType($type)) { - $variableName = $node->variable->name->value; - $context->reportError(new Error( - self::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)), - [ $node->type ] - )); + if (! $type || Type::isInputType($type)) { + return; } - } + + $variableName = $node->variable->name->value; + $context->reportError(new Error( + self::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)), + [$node->type] + )); + }, ]; } + + public static function nonInputTypeOnVarMessage($variableName, $typeName) + { + return sprintf('Variable "$%s" cannot be non-input type "%s".', $variableName, $typeName); + } } diff --git a/src/Validator/Rules/VariablesDefaultValueAllowed.php b/src/Validator/Rules/VariablesDefaultValueAllowed.php index fcbbef4..64808fc 100644 --- a/src/Validator/Rules/VariablesDefaultValueAllowed.php +++ b/src/Validator/Rules/VariablesDefaultValueAllowed.php @@ -1,4 +1,7 @@ function(VariableDefinitionNode $node) use ($context) { - $name = $node->variable->name->value; + NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) use ($context) { + $name = $node->variable->name->value; $defaultValue = $node->defaultValue; - $type = $context->getInputType(); + $type = $context->getInputType(); if ($type instanceof NonNull && $defaultValue) { $context->reportError( - new Error( - self::defaultForRequiredVarMessage( - $name, - $type, - $type->getWrappedType() - ), - [$defaultValue] - ) + new Error( + self::defaultForRequiredVarMessage( + $name, + $type, + $type->getWrappedType() + ), + [$defaultValue] + ) ); } return Visitor::skipNode(); }, - NodeKind::SELECTION_SET => function(SelectionSetNode $node) use ($context) { + NodeKind::SELECTION_SET => function (SelectionSetNode $node) use ($context) { return Visitor::skipNode(); }, - NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) use ($context) { + NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) { return Visitor::skipNode(); }, ]; } + + public static function defaultForRequiredVarMessage($varName, $type, $guessType) + { + return sprintf( + 'Variable "$%s" of type "%s" is required and will not use the default value. Perhaps you meant to use type "%s".', + $varName, + $type, + $guessType + ); + } } diff --git a/src/Validator/Rules/VariablesInAllowedPosition.php b/src/Validator/Rules/VariablesInAllowedPosition.php index c0608ff..a593610 100644 --- a/src/Validator/Rules/VariablesInAllowedPosition.php +++ b/src/Validator/Rules/VariablesInAllowedPosition.php @@ -1,4 +1,7 @@ function () { $this->varDefMap = []; }, - 'leave' => function(OperationDefinitionNode $operation) use ($context) { + 'leave' => function (OperationDefinitionNode $operation) use ($context) { $usages = $context->getRecursiveVariableUsages($operation); foreach ($usages as $usage) { - $node = $usage['node']; - $type = $usage['type']; + $node = $usage['node']; + $type = $usage['type']; $varName = $node->name->value; - $varDef = isset($this->varDefMap[$varName]) ? $this->varDefMap[$varName] : null; + $varDef = $this->varDefMap[$varName] ?? null; - if ($varDef && $type) { - // A var type is allowed if it is the same or more strict (e.g. is - // a subtype of) than the expected type. It can be more strict if - // the variable type is non-null when the expected type is nullable. - // If both are list types, the variable item type can be more strict - // than the expected item type (contravariant). - $schema = $context->getSchema(); - $varType = TypeInfo::typeFromAST($schema, $varDef->type); - - if ($varType && !TypeComparators::isTypeSubTypeOf($schema, $this->effectiveType($varType, $varDef), $type)) { - $context->reportError(new Error( - self::badVarPosMessage($varName, $varType, $type), - [$varDef, $node] - )); - } + if (! $varDef || ! $type) { + continue; } + + // A var type is allowed if it is the same or more strict (e.g. is + // a subtype of) than the expected type. It can be more strict if + // the variable type is non-null when the expected type is nullable. + // If both are list types, the variable item type can be more strict + // than the expected item type (contravariant). + $schema = $context->getSchema(); + $varType = TypeInfo::typeFromAST($schema, $varDef->type); + + if (! $varType || TypeComparators::isTypeSubTypeOf( + $schema, + $this->effectiveType($varType, $varDef), + $type + )) { + continue; + } + + $context->reportError(new Error( + self::badVarPosMessage($varName, $varType, $type), + [$varDef, $node] + )); } - } + }, ], - NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $varDefNode) { + NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $varDefNode) { $this->varDefMap[$varDefNode->variable->name->value] = $varDefNode; - } + }, ]; } - // A var type is allowed if it is the same or more strict than the expected - // type. It can be more strict if the variable type is non-null when the - // expected type is nullable. If both are list types, the variable item type can - // be more strict than the expected item type. + private function effectiveType($varType, $varDef) + { + return (! $varDef->defaultValue || $varType instanceof NonNull) ? $varType : new NonNull($varType); + } + + /** + * A var type is allowed if it is the same or more strict than the expected + * type. It can be more strict if the variable type is non-null when the + * expected type is nullable. If both are list types, the variable item type can + * be more strict than the expected item type. + */ + public static function badVarPosMessage($varName, $varType, $expectedType) + { + return sprintf( + 'Variable "$%s" of type "%s" used in position expecting type "%s".', + $varName, + $varType, + $expectedType + ); + } + + /** If a variable definition has a default value, it's effectively non-null. */ private function varTypeAllowedForType($varType, $expectedType) { if ($expectedType instanceof NonNull) { if ($varType instanceof NonNull) { return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType->getWrappedType()); } + return false; } if ($varType instanceof NonNull) { @@ -80,13 +106,7 @@ class VariablesInAllowedPosition extends AbstractValidationRule if ($varType instanceof ListOfType && $expectedType instanceof ListOfType) { return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType->getWrappedType()); } + return $varType === $expectedType; } - - // If a variable definition has a default value, it's effectively non-null. - private function effectiveType($varType, $varDef) - { - return (!$varDef->defaultValue || $varType instanceof NonNull) ? $varType : new NonNull($varType); - } - } diff --git a/src/Validator/ValidationContext.php b/src/Validator/ValidationContext.php index 4d82ce4..a9c4e4b 100644 --- a/src/Validator/ValidationContext.php +++ b/src/Validator/ValidationContext.php @@ -1,22 +1,27 @@ schema = $schema; - $this->ast = $ast; - $this->typeInfo = $typeInfo; - $this->errors = []; - $this->fragmentSpreads = new SplObjectStorage(); + $this->schema = $schema; + $this->ast = $ast; + $this->typeInfo = $typeInfo; + $this->errors = []; + $this->fragmentSpreads = new SplObjectStorage(); $this->recursivelyReferencedFragments = new SplObjectStorage(); - $this->variableUsages = new SplObjectStorage(); - $this->recursiveVariableUsages = new SplObjectStorage(); + $this->variableUsages = new SplObjectStorage(); + $this->recursiveVariableUsages = new SplObjectStorage(); } - /** - * @param Error $error - */ - function reportError(Error $error) + public function reportError(Error $error) { $this->errors[] = $error; } @@ -100,7 +77,7 @@ class ValidationContext /** * @return Error[] */ - function getErrors() + public function getErrors() { return $this->errors; } @@ -108,159 +85,175 @@ class ValidationContext /** * @return Schema */ - function getSchema() + public function getSchema() { return $this->schema; } /** - * @return DocumentNode + * @return mixed[][] List of ['node' => VariableNode, 'type' => ?InputObjectType] */ - function getDocument() + public function getRecursiveVariableUsages(OperationDefinitionNode $operation) { - return $this->ast; - } + $usages = $this->recursiveVariableUsages[$operation] ?? null; - /** - * @param string $name - * @return FragmentDefinitionNode|null - */ - function getFragment($name) - { - $fragments = $this->fragments; - if (!$fragments) { - $fragments = []; - foreach ($this->getDocument()->definitions as $statement) { - if ($statement->kind === NodeKind::FRAGMENT_DEFINITION) { - $fragments[$statement->name->value] = $statement; - } - } - $this->fragments = $fragments; - } - return isset($fragments[$name]) ? $fragments[$name] : null; - } - - /** - * @param HasSelectionSet $node - * @return FragmentSpreadNode[] - */ - function getFragmentSpreads(HasSelectionSet $node) - { - $spreads = isset($this->fragmentSpreads[$node]) ? $this->fragmentSpreads[$node] : null; - if (!$spreads) { - $spreads = []; - $setsToVisit = [$node->selectionSet]; - while (!empty($setsToVisit)) { - $set = array_pop($setsToVisit); - - for ($i = 0; $i < count($set->selections); $i++) { - $selection = $set->selections[$i]; - if ($selection->kind === NodeKind::FRAGMENT_SPREAD) { - $spreads[] = $selection; - } else if ($selection->selectionSet) { - $setsToVisit[] = $selection->selectionSet; - } - } - } - $this->fragmentSpreads[$node] = $spreads; - } - return $spreads; - } - - /** - * @param OperationDefinitionNode $operation - * @return FragmentDefinitionNode[] - */ - function getRecursivelyReferencedFragments(OperationDefinitionNode $operation) - { - $fragments = isset($this->recursivelyReferencedFragments[$operation]) ? $this->recursivelyReferencedFragments[$operation] : null; - - if (!$fragments) { - $fragments = []; - $collectedNames = []; - $nodesToVisit = [$operation]; - while (!empty($nodesToVisit)) { - $node = array_pop($nodesToVisit); - $spreads = $this->getFragmentSpreads($node); - for ($i = 0; $i < count($spreads); $i++) { - $fragName = $spreads[$i]->name->value; - - if (empty($collectedNames[$fragName])) { - $collectedNames[$fragName] = true; - $fragment = $this->getFragment($fragName); - if ($fragment) { - $fragments[] = $fragment; - $nodesToVisit[] = $fragment; - } - } - } - } - $this->recursivelyReferencedFragments[$operation] = $fragments; - } - return $fragments; - } - - /** - * @param HasSelectionSet $node - * @return array List of ['node' => VariableNode, 'type' => ?InputObjectType] - */ - function getVariableUsages(HasSelectionSet $node) - { - $usages = isset($this->variableUsages[$node]) ? $this->variableUsages[$node] : null; - - if (!$usages) { - $newUsages = []; - $typeInfo = new TypeInfo($this->schema); - Visitor::visit($node, Visitor::visitWithTypeInfo($typeInfo, [ - NodeKind::VARIABLE_DEFINITION => function () { - return false; - }, - NodeKind::VARIABLE => function (VariableNode $variable) use (&$newUsages, $typeInfo) { - $newUsages[] = ['node' => $variable, 'type' => $typeInfo->getInputType()]; - } - ])); - $usages = $newUsages; - $this->variableUsages[$node] = $usages; - } - return $usages; - } - - /** - * @param OperationDefinitionNode $operation - * @return array List of ['node' => VariableNode, 'type' => ?InputObjectType] - */ - function getRecursiveVariableUsages(OperationDefinitionNode $operation) - { - $usages = isset($this->recursiveVariableUsages[$operation]) ? $this->recursiveVariableUsages[$operation] : null; - - if (!$usages) { - $usages = $this->getVariableUsages($operation); + if (! $usages) { + $usages = $this->getVariableUsages($operation); $fragments = $this->getRecursivelyReferencedFragments($operation); $tmp = [$usages]; for ($i = 0; $i < count($fragments); $i++) { $tmp[] = $this->getVariableUsages($fragments[$i]); } - $usages = call_user_func_array('array_merge', $tmp); + $usages = call_user_func_array('array_merge', $tmp); $this->recursiveVariableUsages[$operation] = $usages; } + return $usages; } + /** + * @return mixed[][] List of ['node' => VariableNode, 'type' => ?InputObjectType] + */ + private function getVariableUsages(HasSelectionSet $node) + { + $usages = $this->variableUsages[$node] ?? null; + + if (! $usages) { + $newUsages = []; + $typeInfo = new TypeInfo($this->schema); + Visitor::visit( + $node, + Visitor::visitWithTypeInfo( + $typeInfo, + [ + NodeKind::VARIABLE_DEFINITION => function () { + return false; + }, + NodeKind::VARIABLE => function (VariableNode $variable) use ( + &$newUsages, + $typeInfo + ) { + $newUsages[] = ['node' => $variable, 'type' => $typeInfo->getInputType()]; + }, + ] + ) + ); + $usages = $newUsages; + $this->variableUsages[$node] = $usages; + } + + return $usages; + } + + /** + * @return FragmentDefinitionNode[] + */ + public function getRecursivelyReferencedFragments(OperationDefinitionNode $operation) + { + $fragments = $this->recursivelyReferencedFragments[$operation] ?? null; + + if (! $fragments) { + $fragments = []; + $collectedNames = []; + $nodesToVisit = [$operation]; + while (! empty($nodesToVisit)) { + $node = array_pop($nodesToVisit); + $spreads = $this->getFragmentSpreads($node); + for ($i = 0; $i < count($spreads); $i++) { + $fragName = $spreads[$i]->name->value; + + if (! empty($collectedNames[$fragName])) { + continue; + } + + $collectedNames[$fragName] = true; + $fragment = $this->getFragment($fragName); + if (! $fragment) { + continue; + } + + $fragments[] = $fragment; + $nodesToVisit[] = $fragment; + } + } + $this->recursivelyReferencedFragments[$operation] = $fragments; + } + + return $fragments; + } + + /** + * @return FragmentSpreadNode[] + */ + public function getFragmentSpreads(HasSelectionSet $node) + { + $spreads = $this->fragmentSpreads[$node] ?? null; + if (! $spreads) { + $spreads = []; + $setsToVisit = [$node->selectionSet]; + while (! empty($setsToVisit)) { + $set = array_pop($setsToVisit); + + for ($i = 0; $i < count($set->selections); $i++) { + $selection = $set->selections[$i]; + if ($selection->kind === NodeKind::FRAGMENT_SPREAD) { + $spreads[] = $selection; + } elseif ($selection->selectionSet) { + $setsToVisit[] = $selection->selectionSet; + } + } + } + $this->fragmentSpreads[$node] = $spreads; + } + + return $spreads; + } + + /** + * @param string $name + * @return FragmentDefinitionNode|null + */ + public function getFragment($name) + { + $fragments = $this->fragments; + if (! $fragments) { + $fragments = []; + foreach ($this->getDocument()->definitions as $statement) { + if ($statement->kind !== NodeKind::FRAGMENT_DEFINITION) { + continue; + } + + $fragments[$statement->name->value] = $statement; + } + $this->fragments = $fragments; + } + + return $fragments[$name] ?? null; + } + + /** + * @return DocumentNode + */ + public function getDocument() + { + return $this->ast; + } + /** * Returns OutputType * * @return Type */ - function getType() + public function getType() { return $this->typeInfo->getType(); } /** - * @return CompositeType + * @return Type */ - function getParentType() + public function getParentType() { return $this->typeInfo->getParentType(); } @@ -268,7 +261,7 @@ class ValidationContext /** * @return InputType */ - function getInputType() + public function getInputType() { return $this->typeInfo->getInputType(); } @@ -276,7 +269,7 @@ class ValidationContext /** * @return InputType */ - function getParentInputType() + public function getParentInputType() { return $this->typeInfo->getParentInputType(); } @@ -284,17 +277,17 @@ class ValidationContext /** * @return FieldDefinition */ - function getFieldDef() + public function getFieldDef() { return $this->typeInfo->getFieldDef(); } - function getDirective() + public function getDirective() { return $this->typeInfo->getDirective(); } - function getArgument() + public function getArgument() { return $this->typeInfo->getArgument(); } diff --git a/tests/Validator/QuerySecurityTestCase.php b/tests/Validator/QuerySecurityTestCase.php index cfdb29a..9bb07aa 100644 --- a/tests/Validator/QuerySecurityTestCase.php +++ b/tests/Validator/QuerySecurityTestCase.php @@ -5,7 +5,7 @@ use GraphQL\Error\FormattedError; use GraphQL\Language\Parser; use GraphQL\Type\Introspection; use GraphQL\Validator\DocumentValidator; -use GraphQL\Validator\Rules\AbstractQuerySecurity; +use GraphQL\Validator\Rules\QuerySecurityRule; use PHPUnit\Framework\TestCase; abstract class QuerySecurityTestCase extends TestCase @@ -13,7 +13,7 @@ abstract class QuerySecurityTestCase extends TestCase /** * @param $max * - * @return AbstractQuerySecurity + * @return QuerySecurityRule */ abstract protected function getRule($max); @@ -89,8 +89,9 @@ abstract class QuerySecurityTestCase extends TestCase { $this->assertDocumentValidator($query, $maxExpected); $newMax = $maxExpected - 1; - if ($newMax !== AbstractQuerySecurity::DISABLED) { - $this->assertDocumentValidator($query, $newMax, [$this->createFormattedError($newMax, $maxExpected)]); + if ($newMax === QuerySecurityRule::DISABLED) { + return; } + $this->assertDocumentValidator($query, $newMax, [$this->createFormattedError($newMax, $maxExpected)]); } }