diff --git a/src/Language/AST/FragmentDefinition.php b/src/Language/AST/FragmentDefinition.php index 8715584..095aa68 100644 --- a/src/Language/AST/FragmentDefinition.php +++ b/src/Language/AST/FragmentDefinition.php @@ -2,7 +2,7 @@ namespace GraphQL\Language\AST; -class FragmentDefinition extends Node implements Definition +class FragmentDefinition extends Node implements Definition, HasSelectionSet { public $kind = Node::FRAGMENT_DEFINITION; diff --git a/src/Language/AST/HasSelectionSet.php b/src/Language/AST/HasSelectionSet.php new file mode 100644 index 0000000..54872dd --- /dev/null +++ b/src/Language/AST/HasSelectionSet.php @@ -0,0 +1,10 @@ +kind); + $visitFn = self::getVisitFn($visitor, $node->kind, $isLeaving); if ($visitFn) { $result = call_user_func($visitFn, $node, $key, $parent, $path, $ancestors); @@ -308,13 +306,100 @@ class Visitor return $newRoot; } + /** + * @param $visitors + * @return array + */ + static function visitInParallel($visitors) + { + // TODO: implement real parallel visiting once PHP supports it + $visitorsCount = count($visitors); + $skipping = new \SplFixedArray($visitorsCount); + + return [ + 'enter' => function ($node) use ($visitors, $skipping, $visitorsCount) { + for ($i = 0; $i < $visitorsCount; $i++) { + if (empty($skipping[$i])) { + $fn = self::getVisitFn($visitors[$i], $node->kind, /* isLeaving */ false); + + if ($fn) { + $result = call_user_func_array($fn, func_get_args()); + + if ($result instanceof VisitorOperation) { + if ($result->doContinue) { + $skipping[$i] = $node; + } else if ($result->doBreak) { + $skipping[$i] = $result; + } + } else if ($result !== null) { + return $result; + } + } + } + } + }, + 'leave' => function ($node) use ($visitors, $skipping, $visitorsCount) { + for ($i = 0; $i < $visitorsCount; $i++) { + if (empty($skipping[$i])) { + $fn = self::getVisitFn($visitors[$i], $node->kind, /* isLeaving */ true); + + if ($fn) { + $result = call_user_func_array($fn, func_get_args()); + if ($result instanceof VisitorOperation) { + if ($result->doBreak) { + $skipping[$i] = $result; + } + } else if ($result !== null) { + return $result; + } + } + } else if ($skipping[$i] === $node) { + $skipping[$i] = null; + } + } + } + ]; + } + + /** + * Creates a new visitor instance which maintains a provided TypeInfo instance + * along with visiting visitor. + */ + static function visitWithTypeInfo(TypeInfo $typeInfo, $visitor) + { + return [ + 'enter' => function ($node) use ($typeInfo, $visitor) { + $typeInfo->enter($node); + $fn = self::getVisitFn($visitor, $node->kind, false); + + if ($fn) { + $result = call_user_func_array($fn, func_get_args()); + if ($result) { + $typeInfo->leave($node); + if ($result instanceof Node) { + $typeInfo->enter($result); + } + } + return $result; + } + return null; + }, + 'leave' => function ($node) use ($typeInfo, $visitor) { + $fn = self::getVisitFn($visitor, $node->kind, true); + $result = $fn ? call_user_func_array($fn, func_get_args()) : null; + $typeInfo->leave($node); + return $result; + } + ]; + } + /** * @param $visitor - * @param $isLeaving * @param $kind + * @param $isLeaving * @return null */ - public static function getVisitFn($visitor, $isLeaving, $kind) + public static function getVisitFn($visitor, $kind, $isLeaving) { if (!$visitor) { return null; diff --git a/src/Schema.php b/src/Schema.php index 134d9f8..6940d45 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -1,6 +1,7 @@ querySchema = $querySchema; - $this->mutationSchema = $mutationSchema; - $this->subscriptionSchema = $subscriptionSchema; + /** + * @var array + */ + protected $_typeMap; - InterfaceType::loadImplementationToInterfaces(); + /** + * @var array + */ + protected $_implementations; + + /** + * @var array> + */ + protected $_possibleTypeMap; + + /** + * Schema constructor. + * @param array $config + */ + public function __construct($config = null) + { + if (func_num_args() > 1 || $config instanceof Type) { + trigger_error( + 'GraphQL\Schema constructor expects config object now instead of types passed as arguments. '. + 'See https://github.com/webonyx/graphql-php/issues/36', + E_USER_DEPRECATED + ); + list($queryType, $mutationType, $subscriptionType) = func_get_args(); + + $config = [ + 'query' => $queryType, + 'mutation' => $mutationType, + 'subscription' => $subscriptionType + ]; + } + + $this->_init($config); + } + + protected function _init(array $config) + { + Utils::invariant(isset($config['query']) || isset($config['mutation']), "Either query or mutation type must be set"); + + $config += [ + 'query' => null, + 'mutation' => null, + 'subscription' => null, + 'directives' => [], + 'validate' => true + ]; + + $this->_queryType = $config['query']; + $this->_mutationType = $config['mutation']; + $this->_subscriptionType = $config['subscription']; + + $this->_directives = array_merge($config['directives'], [ + Directive::includeDirective(), + Directive::skipDirective() + ]); // Build type map now to detect any errors within this schema. + $initialTypes = [ + $config['query'], + $config['mutation'], + $config['subscription'], + Introspection::_schema() + ]; + if (!empty($config['types'])) { + $initialTypes = array_merge($initialTypes, $config['types']); + } + $map = []; - foreach ([$this->getQueryType(), $this->getMutationType(), Introspection::_schema()] as $type) { + foreach ($initialTypes as $type) { $this->_extractTypes($type, $map); } $this->_typeMap = $map + Type::getInternalTypes(); + // Keep track of all implementations by interface name. + $this->_implementations = []; + foreach ($this->_typeMap as $typeName => $type) { + if ($type instanceof ObjectType) { + foreach ($type->getInterfaces() as $iface) { + $this->_implementations[$iface->name][] = $type; + } + } + } + + if ($config['validate']) { + $this->validate(); + } + } + + /** + * Additionaly validate schema for integrity + */ + public function validate() + { // Enforce correct interface implementations foreach ($this->_typeMap as $typeName => $type) { if ($type instanceof ObjectType) { foreach ($type->getInterfaces() as $iface) { - $this->assertObjectImplementsInterface($type, $iface); + $this->_assertObjectImplementsInterface($type, $iface); } } } @@ -57,7 +149,7 @@ class Schema * @param InterfaceType $iface * @throws \Exception */ - private function assertObjectImplementsInterface(ObjectType $object, InterfaceType $iface) + protected function _assertObjectImplementsInterface(ObjectType $object, InterfaceType $iface) { $objectFieldMap = $object->getFields(); $ifaceFieldMap = $iface->getFields(); @@ -73,7 +165,7 @@ class Schema $objectField = $objectFieldMap[$fieldName]; Utils::invariant( - $this->isEqualType($ifaceField->getType(), $objectField->getType()), + $this->_isEqualType($ifaceField->getType(), $objectField->getType()), "$iface.$fieldName expects type \"{$ifaceField->getType()}\" but " . "$object.$fieldName provides type \"{$objectField->getType()}" ); @@ -93,7 +185,7 @@ class Schema // Assert interface field arg type matches object field arg type. // (invariant) Utils::invariant( - $this->isEqualType($ifaceArg->getType(), $objectArg->getType()), + $this->_isEqualType($ifaceArg->getType(), $objectArg->getType()), "$iface.$fieldName($argName:) expects type \"{$ifaceArg->getType()}\" " . "but $object.$fieldName($argName:) provides " . "type \"{$objectArg->getType()}\"" @@ -118,35 +210,105 @@ class Schema * @param $typeB * @return bool */ - private function isEqualType($typeA, $typeB) + protected function _isEqualType($typeA, $typeB) { if ($typeA instanceof NonNull && $typeB instanceof NonNull) { - return $this->isEqualType($typeA->getWrappedType(), $typeB->getWrappedType()); + return $this->_isEqualType($typeA->getWrappedType(), $typeB->getWrappedType()); } if ($typeA instanceof ListOfType && $typeB instanceof ListOfType) { - return $this->isEqualType($typeA->getWrappedType(), $typeB->getWrappedType()); + return $this->_isEqualType($typeA->getWrappedType(), $typeB->getWrappedType()); } return $typeA === $typeB; } + /** + * @return ObjectType + */ public function getQueryType() { - return $this->querySchema; + return $this->_queryType; } + /** + * @return ObjectType + */ public function getMutationType() { - return $this->mutationSchema; + return $this->_mutationType; } + /** + * @return ObjectType + */ public function getSubscriptionType() { - return $this->subscriptionSchema; + return $this->_subscriptionType; + } + + /** + * @return array + */ + public function getTypeMap() + { + return $this->_typeMap; + } + + /** + * @param string $name + * @return Type + */ + public function getType($name) + { + $map = $this->getTypeMap(); + return isset($map[$name]) ? $map[$name] : null; + } + + /** + * @param AbstractType $abstractType + * @return ObjectType[] + */ + public function getPossibleTypes(AbstractType $abstractType) + { + if ($abstractType instanceof UnionType) { + return $abstractType->getTypes(); + } + Utils::invariant($abstractType instanceof InterfaceType); + return $this->_implementations[$abstractType->name]; + } + + /** + * @param AbstractType $abstractType + * @param ObjectType $possibleType + * @return bool + */ + public function isPossibleType(AbstractType $abstractType, ObjectType $possibleType) + { + if (null === $this->_possibleTypeMap) { + $this->_possibleTypeMap = []; + } + + if (!isset($this->_possibleTypeMap[$abstractType->name])) { + $tmp = []; + foreach ($this->getPossibleTypes($abstractType) as $type) { + $tmp[$type->name] = true; + } + $this->_possibleTypeMap[$abstractType->name] = $tmp; + } + + return !empty($this->_possibleTypeMap[$abstractType->name][$possibleType->name]); + } + + /** + * @return Directive[] + */ + public function getDirectives() + { + return $this->_directives; } /** * @param $name - * @return null + * @return Directive */ public function getDirective($name) { @@ -158,26 +320,7 @@ class Schema return null; } - /** - * @return array - */ - public function getDirectives() - { - if (!$this->_directives) { - $this->_directives = [ - Directive::includeDirective(), - Directive::skipDirective() - ]; - } - return $this->_directives; - } - - public function getTypeMap() - { - return $this->_typeMap; - } - - private function _extractTypes($type, &$map) + protected function _extractTypes($type, &$map) { if (!$type) { return $map; @@ -198,8 +341,8 @@ class Schema $nestedTypes = []; - if ($type instanceof InterfaceType || $type instanceof UnionType) { - $nestedTypes = $type->getPossibleTypes(); + if ($type instanceof UnionType) { + $nestedTypes = $type->getTypes(); } if ($type instanceof ObjectType) { $nestedTypes = array_merge($nestedTypes, $type->getInterfaces()); @@ -218,10 +361,4 @@ class Schema } return $map; } - - public function getType($name) - { - $map = $this->getTypeMap(); - return isset($map[$name]) ? $map[$name] : null; - } } diff --git a/src/Type/Definition/AbstractType.php b/src/Type/Definition/AbstractType.php index 453af24..7b99cc1 100644 --- a/src/Type/Definition/AbstractType.php +++ b/src/Type/Definition/AbstractType.php @@ -12,16 +12,16 @@ GraphQLUnionType; /** * @return array */ - public function getPossibleTypes(); + // public function getPossibleTypes(); /** * @return ObjectType */ - public function getObjectType($value, ResolveInfo $info); + // public function getObjectType($value, ResolveInfo $info); /** * @param Type $type * @return bool */ - public function isPossibleType(Type $type); + // public function isPossibleType(Type $type); } diff --git a/src/Type/Definition/Directive.php b/src/Type/Definition/Directive.php index abb9b41..ae09422 100644 --- a/src/Type/Definition/Directive.php +++ b/src/Type/Definition/Directive.php @@ -5,6 +5,16 @@ class Directive { public static $internalDirectives; + public static $directiveLocations = [ + 'QUERY' => 'QUERY', + 'MUTATION' => 'MUTATION', + 'SUBSCRIPTION' => 'SUBSCRIPTION', + 'FIELD' => 'FIELD', + 'FRAGMENT_DEFINITION' => 'FRAGMENT_DEFINITION', + 'FRAGMENT_SPREAD' => 'FRAGMENT_SPREAD', + 'INLINE_FRAGMENT' => 'INLINE_FRAGMENT', + ]; + /** * @return Directive */ @@ -30,6 +40,11 @@ class Directive 'include' => new self([ 'name' => 'include', 'description' => 'Directs the executor to include this field or fragment only when the `if` argument is true.', + 'locations' => [ + self::$directiveLocations['FIELD'], + self::$directiveLocations['FRAGMENT_SPREAD'], + self::$directiveLocations['INLINE_FRAGMENT'], + ], 'args' => [ new FieldArgument([ 'name' => 'if', @@ -37,23 +52,22 @@ class Directive 'description' => 'Included when true.' ]) ], - 'onOperation' => false, - 'onFragment' => true, - 'onField' => true ]), 'skip' => new self([ 'name' => 'skip', 'description' => 'Directs the executor to skip this field or fragment when the `if` argument is true.', + 'locations' => [ + self::$directiveLocations['FIELD'], + self::$directiveLocations['FRAGMENT_SPREAD'], + self::$directiveLocations['INLINE_FRAGMENT'] + ], 'args' => [ new FieldArgument([ 'name' => 'if', 'type' => Type::nonNull(Type::boolean()), 'description' => 'Skipped when true' ]) - ], - 'onOperation' => false, - 'onFragment' => true, - 'onField' => true + ] ]) ]; } @@ -70,26 +84,18 @@ class Directive */ public $description; + /** + * Values from self::$locationMap + * + * @var array + */ + public $locations; + /** * @var FieldArgument[] */ public $args; - /** - * @var boolean - */ - public $onOperation; - - /** - * @var boolean - */ - public $onFragment; - - /** - * @var boolean - */ - public $onField; - public function __construct(array $config) { foreach ($config as $key => $value) { diff --git a/src/Type/Definition/UnionType.php b/src/Type/Definition/UnionType.php index cd6cfcf..9fcdac9 100644 --- a/src/Type/Definition/UnionType.php +++ b/src/Type/Definition/UnionType.php @@ -6,7 +6,7 @@ use GraphQL\Utils; class UnionType extends Type implements AbstractType, OutputType, CompositeType { /** - * @var Array + * @var ObjectType[] */ private $_types; @@ -48,10 +48,16 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType $this->_config = $config; } - /** - * @return array - */ public function getPossibleTypes() + { + trigger_error(__METHOD__ . ' is deprecated in favor of ' . __CLASS__ . '::getTypes()', E_USER_DEPRECATED); + return $this->getTypes(); + } + + /** + * @return ObjectType[] + */ + public function getTypes() { if ($this->_types instanceof \Closure) { $this->_types = call_user_func($this->_types); @@ -71,7 +77,7 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType if (null === $this->_possibleTypeNames) { $this->_possibleTypeNames = []; - foreach ($this->getPossibleTypes() as $possibleType) { + foreach ($this->getTypes() as $possibleType) { $this->_possibleTypeNames[$possibleType->name] = true; } } diff --git a/src/Utils/TypeInfo.php b/src/Utils/TypeInfo.php index 46ed40c..145b631 100644 --- a/src/Utils/TypeInfo.php +++ b/src/Utils/TypeInfo.php @@ -41,7 +41,7 @@ class TypeInfo return $innerType ? new NonNull($innerType) : null; } - Utils::invariant($inputTypeAst->kind === Node::NAMED_TYPE, 'Must be a named type'); + Utils::invariant($inputTypeAst && $inputTypeAst->kind === Node::NAMED_TYPE, 'Must be a named type'); return $schema->getType($inputTypeAst->name->value); } @@ -197,11 +197,7 @@ class TypeInfo // isCompositeType is a type refining predicate, so this is safe. $compositeType = $namedType; } - array_push($this->_parentTypeStack, $compositeType); - break; - - case Node::DIRECTIVE: - $this->_directive = $schema->getDirective($node->name->value); + $this->_parentTypeStack[] = $compositeType; // push break; case Node::FIELD: @@ -210,8 +206,12 @@ class TypeInfo if ($parentType) { $fieldDef = self::_getFieldDef($schema, $parentType, $node); } - array_push($this->_fieldDefStack, $fieldDef); - array_push($this->_typeStack, $fieldDef ? $fieldDef->getType() : null); + $this->_fieldDefStack[] = $fieldDef; // push + $this->_typeStack[] = $fieldDef ? $fieldDef->getType() : null; // push + break; + + case Node::DIRECTIVE: + $this->_directive = $schema->getDirective($node->name->value); break; case Node::OPERATION_DEFINITION: @@ -220,18 +220,22 @@ class TypeInfo $type = $schema->getQueryType(); } else if ($node->operation === 'mutation') { $type = $schema->getMutationType(); + } else if ($node->operation === 'subscription') { + $type = $schema->getSubscriptionType(); } - array_push($this->_typeStack, $type); + $this->_typeStack[] = $type; // push break; case Node::INLINE_FRAGMENT: case Node::FRAGMENT_DEFINITION: - $type = self::typeFromAST($schema, $node->typeCondition); - array_push($this->_typeStack, $type); + $typeConditionAST = $node->typeCondition; + $outputType = $typeConditionAST ? self::typeFromAST($schema, $typeConditionAST) : $this->getType(); + $this->_typeStack[] = $outputType; // push break; case Node::VARIABLE_DEFINITION: - array_push($this->_inputTypeStack, self::typeFromAST($schema, $node->type)); + $inputType = self::typeFromAST($schema, $node->type); + $this->_inputTypeStack[] = $inputType; // push break; case Node::ARGUMENT: @@ -244,15 +248,12 @@ class TypeInfo } } $this->_argument = $argDef; - array_push($this->_inputTypeStack, $argType); + $this->_inputTypeStack[] = $argType; // push break; case Node::LST: $listType = Type::getNullableType($this->getInputType()); - array_push( - $this->_inputTypeStack, - $listType instanceof ListOfType ? $listType->getWrappedType() : null - ); + $this->_inputTypeStack[] = ($listType instanceof ListOfType ? $listType->getWrappedType() : null); // push break; case Node::OBJECT_FIELD: @@ -263,7 +264,7 @@ class TypeInfo $inputField = isset($tmp[$node->name->value]) ? $tmp[$node->name->value] : null; $fieldType = $inputField ? $inputField->getType() : null; } - array_push($this->_inputTypeStack, $fieldType); + $this->_inputTypeStack[] = $fieldType; break; } } diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index 19c7c50..2e1c828 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -8,11 +8,13 @@ use GraphQL\Language\AST\FragmentSpread; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Value; use GraphQL\Language\AST\Variable; +use GraphQL\Language\Printer; use GraphQL\Language\Visitor; use GraphQL\Language\VisitorOperation; use GraphQL\Schema; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\InputObjectType; +use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ScalarType; @@ -27,6 +29,7 @@ use GraphQL\Validator\Rules\KnownArgumentNames; use GraphQL\Validator\Rules\KnownDirectives; use GraphQL\Validator\Rules\KnownFragmentNames; use GraphQL\Validator\Rules\KnownTypeNames; +use GraphQL\Validator\Rules\LoneAnonymousOperation; use GraphQL\Validator\Rules\NoFragmentCycles; use GraphQL\Validator\Rules\NoUndefinedVariables; use GraphQL\Validator\Rules\NoUnusedFragments; @@ -62,28 +65,31 @@ class DocumentValidator { if (null === self::$defaultRules) { self::$defaultRules = [ - // new UniqueOperationNames, - // new LoneAnonymousOperation, + // 'UniqueOperationNames' => new UniqueOperationNames(), + 'LoneAnonymousOperation' => new LoneAnonymousOperation(), 'KnownTypeNames' => new KnownTypeNames(), 'FragmentsOnCompositeTypes' => new FragmentsOnCompositeTypes(), 'VariablesAreInputTypes' => new VariablesAreInputTypes(), 'ScalarLeafs' => new ScalarLeafs(), 'FieldsOnCorrectType' => new FieldsOnCorrectType(), - // new UniqueFragmentNames, + // 'UniqueFragmentNames' => new UniqueFragmentNames(), 'KnownFragmentNames' => new KnownFragmentNames(), 'NoUnusedFragments' => new NoUnusedFragments(), 'PossibleFragmentSpreads' => new PossibleFragmentSpreads(), 'NoFragmentCycles' => new NoFragmentCycles(), + // 'UniqueVariableNames' => new UniqueVariableNames(), 'NoUndefinedVariables' => new NoUndefinedVariables(), 'NoUnusedVariables' => new NoUnusedVariables(), 'KnownDirectives' => new KnownDirectives(), 'KnownArgumentNames' => new KnownArgumentNames(), - // new UniqueArgumentNames, + // 'UniqueArgumentNames' => new UniqueArgumentNames(), 'ArgumentsOfCorrectType' => new ArgumentsOfCorrectType(), 'ProvidedNonNullArguments' => new ProvidedNonNullArguments(), 'DefaultValuesOfCorrectType' => new DefaultValuesOfCorrectType(), 'VariablesInAllowedPosition' => new VariablesInAllowedPosition(), 'OverlappingFieldsCanBeMerged' => new OverlappingFieldsCanBeMerged(), + // 'UniqueInputFieldNames' => new UniqueInputFieldNames(), + // Query Security 'QueryDepth' => new QueryDepth(QueryDepth::DISABLED), // default disabled 'QueryComplexity' => new QueryComplexity(QueryComplexity::DISABLED), // default disabled @@ -107,7 +113,8 @@ class DocumentValidator public static function validate(Schema $schema, Document $ast, array $rules = null) { - $errors = static::visitUsingRules($schema, $ast, $rules ?: static::allRules()); + $typeInfo = new TypeInfo($schema); + $errors = static::visitUsingRules($schema, $typeInfo, $ast, $rules ?: static::allRules()); return $errors; } @@ -128,76 +135,109 @@ class DocumentValidator return $arr; } - public static function isValidLiteralValue($valueAST, Type $type) + /** + * Utility for validators which determines if a value literal AST is valid given + * an input type. + * + * Note that this only validates literal values, variables are assumed to + * provide values of the correct type. + * + * @return array + */ + public static function isValidLiteralValue(Type $type, $valueAST) { - // A value can only be not provided if the type is nullable. - if (!$valueAST) { - return !($type instanceof NonNull); + // A value must be provided if the type is non-null. + if ($type instanceof NonNull) { + $wrappedType = $type->getWrappedType(); + if (!$valueAST) { + if ($wrappedType->name) { + return [ "Expected \"{$wrappedType->name}!\", found null." ]; + } + return ['Expected non-null value, found null.']; + } + return static::isValidLiteralValue($wrappedType, $valueAST); } - // Unwrap non-null. - if ($type instanceof NonNull) { - return static::isValidLiteralValue($valueAST, $type->getWrappedType()); + if (!$valueAST) { + return []; } // This function only tests literals, and assumes variables will provide // values of the correct type. if ($valueAST instanceof Variable) { - return true; - } - - if (!$valueAST instanceof Value) { - return false; + return []; } // Lists accept a non-list value as a list of one. if ($type instanceof ListOfType) { $itemType = $type->getWrappedType(); if ($valueAST instanceof ListValue) { - foreach($valueAST->values as $itemAST) { - if (!static::isValidLiteralValue($itemAST, $itemType)) { - return false; + $errors = []; + foreach($valueAST->values as $index => $itemAST) { + $tmp = static::isValidLiteralValue($itemType, $itemAST); + + if ($tmp) { + $errors = array_merge($errors, Utils::map($tmp, function($error) use ($index) { + return "In element #$index: $error"; + })); } } - return true; + return $errors; } else { - return static::isValidLiteralValue($valueAST, $itemType); + return static::isValidLiteralValue($itemType, $valueAST); } } - // Scalar/Enum input checks to ensure the type can serialize the value to - // a non-null value. - if ($type instanceof ScalarType || $type instanceof EnumType) { - return $type->parseLiteral($valueAST) !== null; - } - - // Input objects check each defined field, ensuring it is of the correct - // type and provided if non-nullable. + // Input objects check each defined field and look for undefined fields. if ($type instanceof InputObjectType) { - $fields = $type->getFields(); if ($valueAST->kind !== Node::OBJECT) { - return false; + return [ "Expected \"{$type->name}\", found not an object." ]; } - $fieldASTs = $valueAST->fields; - $fieldASTMap = Utils::keyMap($fieldASTs, function($field) {return $field->name->value;}); - foreach ($fields as $fieldKey => $field) { - $fieldName = $field->name ?: $fieldKey; - if (!isset($fieldASTMap[$fieldName]) && $field->getType() instanceof NonNull) { - // Required fields missing - return false; + $fields = $type->getFields(); + $errors = []; + + // Ensure every provided field is defined. + $fieldASTs = $valueAST->fields; + + foreach ($fieldASTs as $providedFieldAST) { + if (empty($fields[$providedFieldAST->name->value])) { + $errors[] = "In field \"{$providedFieldAST->name->value}\": Unknown field."; } } - foreach ($fieldASTs as $fieldAST) { - if (empty($fields[$fieldAST->name->value]) || !static::isValidLiteralValue($fieldAST->value, $fields[$fieldAST->name->value]->getType())) { - return false; + + // Ensure every defined field is valid. + $fieldASTMap = Utils::keyMap($fieldASTs, function($fieldAST) {return $fieldAST->name->value;}); + foreach ($fields as $fieldName => $field) { + $result = static::isValidLiteralValue( + $field->getType(), + isset($fieldASTMap[$fieldName]) ? $fieldASTMap[$fieldName]->value : null + ); + if ($result) { + $errors = array_merge($errors, Utils::map($result, function($error) use ($fieldName) { + return "In field \"$fieldName\": $error"; + })); } } - return true; + + return $errors; } - // Any other kind of type is not an input type, and a literal cannot be used. - return false; + Utils::invariant( + $type instanceof ScalarType || $type instanceof EnumType, + 'Must be input type' + ); + + // Scalar/Enum input checks to ensure the type can parse the value to + // a non-null value. + $parseResult = $type->parseLiteral($valueAST); + + if (null === $parseResult) { + $printed = Printer::doPrint($valueAST); + return [ "Expected type \"{$type->name}\", found $printed." ]; + } + + return []; } /** @@ -205,14 +245,23 @@ class DocumentValidator * while maintaining the visitor skip and break API. * * @param Schema $schema + * @param TypeInfo $typeInfo * @param Document $documentAST * @param array $rules * @return array */ - public static function visitUsingRules(Schema $schema, Document $documentAST, array $rules) + public static function visitUsingRules(Schema $schema, TypeInfo $typeInfo, Document $documentAST, array $rules) { - $typeInfo = new TypeInfo($schema); $context = new ValidationContext($schema, $documentAST, $typeInfo); + $visitors = []; + foreach ($rules as $rule) { + $visitors[] = $rule($context); + } + Visitor::visit($documentAST, Visitor::visitWithTypeInfo($typeInfo, Visitor::visitInParallel($visitors))); + return $context->getErrors(); + + + $errors = []; // TODO: convert to class @@ -237,7 +286,7 @@ class DocumentValidator if ($node->kind === Node::FRAGMENT_DEFINITION && $key !== null && !empty($instances[$i]['visitSpreadFragments'])) { $result = Visitor::skipNode(); } else { - $enter = Visitor::getVisitFn($instances[$i], false, $node->kind); + $enter = Visitor::getVisitFn($instances[$i], $node->kind, false); if ($enter instanceof \Closure) { // $enter = $enter->bindTo($instances[$i]); $result = call_user_func_array($enter, func_get_args()); @@ -266,7 +315,7 @@ class DocumentValidator } else if ($result && static::isError($result)) { static::append($errors, $result); for ($j = $i - 1; $j >= 0; $j--) { - $leaveFn = Visitor::getVisitFn($instances[$j], true, $node->kind); + $leaveFn = Visitor::getVisitFn($instances[$j], $node->kind, true); if ($leaveFn) { // $leaveFn = $leaveFn->bindTo($instances[$j]) $result = call_user_func_array($leaveFn, func_get_args()); @@ -316,7 +365,7 @@ class DocumentValidator } continue; } - $leaveFn = Visitor::getVisitFn($instances[$i], true, $node->kind); + $leaveFn = Visitor::getVisitFn($instances[$i], $node->kind, true); if ($leaveFn) { // $leaveFn = $leaveFn.bindTo($instances[$i]); diff --git a/src/Validator/Messages.php b/src/Validator/Messages.php index a8bb5ed..8ff38a4 100644 --- a/src/Validator/Messages.php +++ b/src/Validator/Messages.php @@ -1,6 +1,8 @@ function(Argument $argAST) use ($context) { $argDef = $context->getArgument(); - if ($argDef && !DocumentValidator::isValidLiteralValue($argAST->value, $argDef->getType())) { - return new Error( - self::badValueMessage($argAST->name->value, $argDef->getType(), Printer::doPrint($argAST->value)), - [$argAST->value] - ); + if ($argDef) { + $errors = DocumentValidator::isValidLiteralValue($argDef->getType(), $argAST->value); + + if (!empty($errors)) { + $context->reportError(new Error( + self::badValueMessage($argAST->name->value, $argDef->getType(), Printer::doPrint($argAST->value), $errors), + [$argAST->value] + )); + } } + return Visitor::skipNode(); } ]; } diff --git a/src/Validator/Rules/DefaultValuesOfCorrectType.php b/src/Validator/Rules/DefaultValuesOfCorrectType.php index 1f29991..22961b7 100644 --- a/src/Validator/Rules/DefaultValuesOfCorrectType.php +++ b/src/Validator/Rules/DefaultValuesOfCorrectType.php @@ -13,6 +13,19 @@ use GraphQL\Validator\ValidationContext; class DefaultValuesOfCorrectType { + static function badValueForDefaultArgMessage($varName, $type, $value, $verboseErrors = null) + { + $message = $verboseErrors ? ("\n" . implode("\n", $verboseErrors)) : ''; + return "Variable \$$varName has invalid default value: $value.$message"; + } + + static function defaultForNonNullArgMessage($varName, $type, $guessType) + { + return "Variable \$$varName of type $type " . + "is required and will never use the default value. " . + "Perhaps you meant to use type $guessType."; + } + public function __invoke(ValidationContext $context) { return [ @@ -22,16 +35,19 @@ class DefaultValuesOfCorrectType $type = $context->getInputType(); if ($type instanceof NonNull && $defaultValue) { - return new Error( - Messages::defaultForNonNullArgMessage($name, $type, $type->getWrappedType()), + $context->reportError(new Error( + static::defaultForNonNullArgMessage($name, $type, $type->getWrappedType()), [$defaultValue] - ); + )); } - if ($type && $defaultValue && !DocumentValidator::isValidLiteralValue($defaultValue, $type)) { - return new Error( - Messages::badValueForDefaultArgMessage($name, $type, Printer::doPrint($defaultValue)), - [$defaultValue] - ); + if ($type && $defaultValue) { + $errors = DocumentValidator::isValidLiteralValue($type, $defaultValue); + if (!empty($errors)) { + $context->reportError(new Error( + static::badValueForDefaultArgMessage($name, $type, Printer::doPrint($defaultValue), $errors), + [$defaultValue] + )); + } } return null; } diff --git a/src/Validator/Rules/FieldsOnCorrectType.php b/src/Validator/Rules/FieldsOnCorrectType.php index ee91b47..9422388 100644 --- a/src/Validator/Rules/FieldsOnCorrectType.php +++ b/src/Validator/Rules/FieldsOnCorrectType.php @@ -5,11 +5,34 @@ namespace GraphQL\Validator\Rules; use GraphQL\Error; use GraphQL\Language\AST\Field; use GraphQL\Language\AST\Node; +use GraphQL\Schema; +use GraphQL\Type\Definition\AbstractType; +use GraphQL\Utils; use GraphQL\Validator\Messages; use GraphQL\Validator\ValidationContext; class FieldsOnCorrectType { + static function undefinedFieldMessage($field, $type, array $suggestedTypes = []) + { + $message = 'Cannot query field "' . $field . '" on type "' . $type.'".'; + + $maxLength = 5; + $count = count($suggestedTypes); + if ($count > 0) { + $suggestions = array_slice($suggestedTypes, 0, $maxLength); + $suggestions = Utils::map($suggestions, function($t) { return "\"$t\""; }); + $suggestions = implode(', ', $suggestions); + + if ($count > $maxLength) { + $suggestions .= ', and ' . ($count - $maxLength) . ' other types'; + } + $message .= " However, this field exists on $suggestions."; + $message .= ' Perhaps you meant to use an inline fragment?'; + } + return $message; + } + public function __invoke(ValidationContext $context) { return [ @@ -18,13 +41,71 @@ class FieldsOnCorrectType if ($type) { $fieldDef = $context->getFieldDef(); if (!$fieldDef) { - return new Error( - Messages::undefinedFieldMessage($node->name->value, $type->name), + // This isn't valid. Let's find suggestions, if any. + $suggestedTypes = []; + if ($type instanceof AbstractType) { + $schema = $context->getSchema(); + $suggestedTypes = self::getSiblingInterfacesIncludingField( + $schema, + $type, + $node->name->value + ); + $suggestedTypes = array_merge($suggestedTypes, + self::getImplementationsIncludingField($schema, $type, $node->name->value) + ); + } + $context->reportError(new Error( + static::undefinedFieldMessage($node->name->value, $type->name, $suggestedTypes), [$node] - ); + )); } } } ]; } + + /** + * Return implementations of `type` that include `fieldName` as a valid field. + * + * @param Schema $schema + * @param AbstractType $type + * @param $fieldName + * @return array + */ + static function getImplementationsIncludingField(Schema $schema, AbstractType $type, $fieldName) + { + $types = $schema->getPossibleTypes($type); + $types = Utils::filter($types, function($t) use ($fieldName) {return isset($t->getFields()[$fieldName]);}); + $types = Utils::map($types, function($t) {return $t->name;}); + sort($types); + return $types; + } + + /** + * Go through all of the implementations of type, and find other interaces + * that they implement. If those interfaces include `field` as a valid field, + * return them, sorted by how often the implementations include the other + * interface. + */ + static function getSiblingInterfacesIncludingField(Schema $schema, AbstractType $type, $fieldName) + { + $types = $schema->getPossibleTypes($type); + $suggestedInterfaces = array_reduce($types, function ($acc, $t) use ($fieldName) { + foreach ($t->getInterfaces() as $i) { + if (empty($i->getFields()[$fieldName])) { + continue; + } + if (!isset($acc[$i->name])) { + $acc[$i->name] = 0; + } + $acc[$i->name] += 1; + } + return $acc; + }, []); + $suggestedInterfaceNames = array_keys($suggestedInterfaces); + usort($suggestedInterfaceNames, function($a, $b) use ($suggestedInterfaces) { + return $suggestedInterfaces[$b] - $suggestedInterfaces[$a]; + }); + return $suggestedInterfaceNames; + } } diff --git a/src/Validator/Rules/FragmentsOnCompositeTypes.php b/src/Validator/Rules/FragmentsOnCompositeTypes.php index f3cee89..c9779cd 100644 --- a/src/Validator/Rules/FragmentsOnCompositeTypes.php +++ b/src/Validator/Rules/FragmentsOnCompositeTypes.php @@ -30,21 +30,21 @@ class FragmentsOnCompositeTypes Node::INLINE_FRAGMENT => function(InlineFragment $node) use ($context) { $type = $context->getType(); - if ($type && !Type::isCompositeType($type)) { - return new Error( - self::inlineFragmentOnNonCompositeErrorMessage($type), + if ($node->typeCondition && $type && !Type::isCompositeType($type)) { + $context->reportError(new Error( + static::inlineFragmentOnNonCompositeErrorMessage($type), [$node->typeCondition] - ); + )); } }, Node::FRAGMENT_DEFINITION => function(FragmentDefinition $node) use ($context) { $type = $context->getType(); if ($type && !Type::isCompositeType($type)) { - return new Error( - self::fragmentOnNonCompositeErrorMessage($node->name->value, Printer::doPrint($node->typeCondition)), + $context->reportError(new Error( + static::fragmentOnNonCompositeErrorMessage($node->name->value, Printer::doPrint($node->typeCondition)), [$node->typeCondition] - ); + )); } } ]; diff --git a/src/Validator/Rules/KnownArgumentNames.php b/src/Validator/Rules/KnownArgumentNames.php index c610573..a6f0ea0 100644 --- a/src/Validator/Rules/KnownArgumentNames.php +++ b/src/Validator/Rules/KnownArgumentNames.php @@ -40,10 +40,10 @@ class KnownArgumentNames if (!$fieldArgDef) { $parentType = $context->getParentType(); Utils::invariant($parentType); - return new Error( + $context->reportError(new Error( self::unknownArgMessage($node->name->value, $fieldDef->name, $parentType->name), [$node] - ); + )); } } } else if ($argumentOf->kind === Node::DIRECTIVE) { @@ -57,10 +57,10 @@ class KnownArgumentNames } } if (!$directiveArgDef) { - return new Error( + $context->reportError(new Error( self::unknownDirectiveArgMessage($node->name->value, $directive->name), [$node] - ); + )); } } } diff --git a/src/Validator/Rules/KnownDirectives.php b/src/Validator/Rules/KnownDirectives.php index da8d3af..8c3ad4b 100644 --- a/src/Validator/Rules/KnownDirectives.php +++ b/src/Validator/Rules/KnownDirectives.php @@ -12,6 +12,7 @@ use GraphQL\Language\AST\Node; use GraphQL\Language\AST\OperationDefinition; use GraphQL\Validator\Messages; use GraphQL\Validator\ValidationContext; +use GraphQL\Type\Definition\Directive as DirectiveDef; class KnownDirectives { @@ -20,9 +21,9 @@ class KnownDirectives return "Unknown directive \"$directiveName\"."; } - static function misplacedDirectiveMessage($directiveName, $placement) + static function misplacedDirectiveMessage($directiveName, $location) { - return "Directive \"$directiveName\" may not be used on \"$placement\"."; + return "Directive \"$directiveName\" may not be used on \"$location\"."; } public function __invoke(ValidationContext $context) @@ -38,39 +39,44 @@ class KnownDirectives } if (!$directiveDef) { - return new Error( + $context->reportError(new Error( self::unknownDirectiveMessage($node->name->value), [$node] - ); + )); + return ; } $appliedTo = $ancestors[count($ancestors) - 1]; + $candidateLocation = $this->getLocationForAppliedNode($appliedTo); - if ($appliedTo instanceof OperationDefinition && !$directiveDef->onOperation) { - return new Error( - self::misplacedDirectiveMessage($node->name->value, 'operation'), + if (!$candidateLocation) { + $context->reportError(new Error( + self::misplacedDirectiveMessage($node->name->value, $node->type), [$node] - ); - } - if ($appliedTo instanceof Field && !$directiveDef->onField) { - return new Error( - self::misplacedDirectiveMessage($node->name->value, 'field'), - [$node] - ); - } - - $fragmentKind = ( - $appliedTo instanceof FragmentSpread || - $appliedTo instanceof InlineFragment || - $appliedTo instanceof FragmentDefinition - ); - - if ($fragmentKind && !$directiveDef->onFragment) { - return new Error( - self::misplacedDirectiveMessage($node->name->value, 'fragment'), - [$node] - ); + )); + } else if (!in_array($candidateLocation, $directiveDef->locations)) { + $context->reportError(new Error( + self::misplacedDirectiveMessage($node->name->value, $candidateLocation), + [ $node ] + )); } } ]; } + + private function getLocationForAppliedNode(Node $appliedTo) + { + switch ($appliedTo->kind) { + case Node::OPERATION_DEFINITION: + switch ($appliedTo->operation) { + case 'query': return DirectiveDef::$directiveLocations['QUERY']; + case 'mutation': return DirectiveDef::$directiveLocations['MUTATION']; + case 'subscription': return DirectiveDef::$directiveLocations['SUBSCRIPTION']; + } + break; + case Node::FIELD: return DirectiveDef::$directiveLocations['FIELD']; + case Node::FRAGMENT_SPREAD: return DirectiveDef::$directiveLocations['FRAGMENT_SPREAD']; + case Node::INLINE_FRAGMENT: return DirectiveDef::$directiveLocations['INLINE_FRAGMENT']; + case Node::FRAGMENT_DEFINITION: return DirectiveDef::$directiveLocations['FRAGMENT_DEFINITION']; + } + } } diff --git a/src/Validator/Rules/KnownFragmentNames.php b/src/Validator/Rules/KnownFragmentNames.php index 3d0dbc8..6361017 100644 --- a/src/Validator/Rules/KnownFragmentNames.php +++ b/src/Validator/Rules/KnownFragmentNames.php @@ -21,10 +21,10 @@ class KnownFragmentNames $fragmentName = $node->name->value; $fragment = $context->getFragment($fragmentName); if (!$fragment) { - return new Error( + $context->reportError(new Error( self::unknownFragmentMessage($fragmentName), [$node->name] - ); + )); } } ]; diff --git a/src/Validator/Rules/KnownTypeNames.php b/src/Validator/Rules/KnownTypeNames.php index 93d5f4c..95f1437 100644 --- a/src/Validator/Rules/KnownTypeNames.php +++ b/src/Validator/Rules/KnownTypeNames.php @@ -6,6 +6,7 @@ use GraphQL\Error; use GraphQL\Language\AST\Name; use GraphQL\Language\AST\NamedType; use GraphQL\Language\AST\Node; +use GraphQL\Language\Visitor; use GraphQL\Validator\Messages; use GraphQL\Validator\ValidationContext; @@ -18,14 +19,19 @@ class KnownTypeNames public function __invoke(ValidationContext $context) { + $skip = function() {return Visitor::skipNode();}; + return [ + Node::OBJECT_TYPE_DEFINITION => $skip, + Node::INTERFACE_TYPE_DEFINITION => $skip, + Node::UNION_TYPE_DEFINITION => $skip, + Node::INPUT_OBJECT_TYPE_DEFINITION => $skip, + Node::NAMED_TYPE => function(NamedType $node, $key) use ($context) { - if ($key === 'type' || $key === 'typeCondition') { - $typeName = $node->name->value; - $type = $context->getSchema()->getType($typeName); - if (!$type) { - return new Error(self::unknownTypeMessage($typeName), [$node]); - } + $typeName = $node->name->value; + $type = $context->getSchema()->getType($typeName); + if (!$type) { + $context->reportError(new Error(self::unknownTypeMessage($typeName), [$node])); } } ]; diff --git a/src/Validator/Rules/LoneAnonymousOperation.php b/src/Validator/Rules/LoneAnonymousOperation.php new file mode 100644 index 0000000..09d40d8 --- /dev/null +++ b/src/Validator/Rules/LoneAnonymousOperation.php @@ -0,0 +1,46 @@ + function(Document $node) use (&$operationCount) { + $tmp = Utils::filter( + $node->definitions, + function ($definition) { + return $definition->kind === Node::OPERATION_DEFINITION; + } + ); + $operationCount = count($tmp); + }, + Node::OPERATION_DEFINITION => function(OperationDefinition $node) use (&$operationCount, $context) { + if (!$node->name && $operationCount > 1) { + $context->reportError( + new Error(self::anonOperationNotAloneMessage(), [$node]) + ); + } + } + ]; + } +} diff --git a/src/Validator/Rules/NoFragmentCycles.php b/src/Validator/Rules/NoFragmentCycles.php index 8f41fb8..addf8d7 100644 --- a/src/Validator/Rules/NoFragmentCycles.php +++ b/src/Validator/Rules/NoFragmentCycles.php @@ -14,6 +14,7 @@ use GraphQL\Language\AST\FragmentDefinition; use GraphQL\Language\AST\FragmentSpread; use GraphQL\Language\AST\Node; use GraphQL\Language\Visitor; +use GraphQL\Utils; use GraphQL\Validator\ValidationContext; class NoFragmentCycles @@ -24,83 +25,86 @@ class NoFragmentCycles return "Cannot spread fragment \"$fragName\" within itself$via."; } + public $visitedFrags; + + public $spreadPath; + + public $spreadPathIndexByName; + public function __invoke(ValidationContext $context) { - // Gather all the fragment spreads ASTs for each fragment definition. - // Importantly this does not include inline fragments. - $definitions = $context->getDocument()->definitions; - $spreadsInFragment = []; - foreach ($definitions as $node) { - if ($node instanceof FragmentDefinition) { - $spreadsInFragment[$node->name->value] = $this->gatherSpreads($node); - } - } + // Tracks already visited fragments to maintain O(N) and to ensure that cycles + // are not redundantly reported. + $this->visitedFrags = []; - // Tracks spreads known to lead to cycles to ensure that cycles are not - // redundantly reported. - $knownToLeadToCycle = new \SplObjectStorage(); + // Array of AST nodes used to produce meaningful errors + $this->spreadPath = []; + + // Position in the spread path + $this->spreadPathIndexByName = []; return [ - Node::FRAGMENT_DEFINITION => function(FragmentDefinition $node) use ($spreadsInFragment, $knownToLeadToCycle) { - $errors = []; - $initialName = $node->name->value; - - // Array of AST nodes used to produce meaningful errors - $spreadPath = []; - - $this->detectCycleRecursive($initialName, $spreadsInFragment, $knownToLeadToCycle, $initialName, $spreadPath, $errors); - - if (!empty($errors)) { - return $errors; + Node::OPERATION_DEFINITION => function () { + return Visitor::skipNode(); + }, + Node::FRAGMENT_DEFINITION => function (FragmentDefinition $node) use ($context) { + if (!isset($this->visitedFrags[$node->name->value])) { + $this->detectCycleRecursive($node, $context); } + return Visitor::skipNode(); } ]; } - private function detectCycleRecursive($fragmentName, array $spreadsInFragment, \SplObjectStorage $knownToLeadToCycle, $initialName, array &$spreadPath, &$errors) + private function detectCycleRecursive(FragmentDefinition $fragment, ValidationContext $context) { - $spreadNodes = $spreadsInFragment[$fragmentName]; + $fragmentName = $fragment->name->value; + $this->visitedFrags[$fragmentName] = true; - for ($i = 0; $i < count($spreadNodes); ++$i) { - $spreadNode = $spreadNodes[$i]; - if (isset($knownToLeadToCycle[$spreadNode])) { - continue ; - } - if ($spreadNode->name->value === $initialName) { - $cyclePath = array_merge($spreadPath, [$spreadNode]); - foreach ($cyclePath as $spread) { - $knownToLeadToCycle[$spread] = true; - } - $errors[] = new Error( - self::cycleErrorMessage($initialName, array_map(function ($s) { - return $s->name->value; - }, $spreadPath)), - $cyclePath - ); - continue; - } + $spreadNodes = $context->getFragmentSpreads($fragment); - foreach ($spreadPath as $spread) { - if ($spread === $spreadNode) { - continue 2; - } - } - - $spreadPath[] = $spreadNode; - $this->detectCycleRecursive($spreadNode->name->value, $spreadsInFragment, $knownToLeadToCycle, $initialName, $spreadPath, $errors); - array_pop($spreadPath); + if (empty($spreadNodes)) { + return; } - } + $this->spreadPathIndexByName[$fragmentName] = count($this->spreadPath); - private function gatherSpreads($node) - { - $spreadNodes = []; - Visitor::visit($node, [ - Node::FRAGMENT_SPREAD => function(FragmentSpread $spread) use (&$spreadNodes) { - $spreadNodes[] = $spread; + for ($i = 0; $i < count($spreadNodes); $i++) { + $spreadNode = $spreadNodes[$i]; + $spreadName = $spreadNode->name->value; + $cycleIndex = isset($this->spreadPathIndexByName[$spreadName]) ? $this->spreadPathIndexByName[$spreadName] : null; + + if ($cycleIndex === null) { + $this->spreadPath[] = $spreadNode; + if (empty($this->visitedFrags[$spreadName])) { + $spreadFragment = $context->getFragment($spreadName); + if ($spreadFragment) { + $this->detectCycleRecursive($spreadFragment, $context); + } + } + array_pop($this->spreadPath); + } else { + $cyclePath = array_slice($this->spreadPath, $cycleIndex); + $nodes = $cyclePath; + + if (is_array($spreadNode)) { + $nodes = array_merge($nodes, $spreadNode); + } else { + $nodes[] = $spreadNode; + } + + $context->reportError(new Error( + self::cycleErrorMessage( + $spreadName, + Utils::map($cyclePath, function ($s) { + return $s->name->value; + }) + ), + $nodes + )); } - ]); - return $spreadNodes; + } + + $this->spreadPathIndexByName[$fragmentName] = null; } } diff --git a/src/Validator/Rules/NoUndefinedVariables.php b/src/Validator/Rules/NoUndefinedVariables.php index dca3ad9..9d7ab5a 100644 --- a/src/Validator/Rules/NoUndefinedVariables.php +++ b/src/Validator/Rules/NoUndefinedVariables.php @@ -23,62 +23,43 @@ use GraphQL\Validator\ValidationContext; */ class NoUndefinedVariables { - static function undefinedVarMessage($varName) + static function undefinedVarMessage($varName, $opName = null) { - return "Variable \"$$varName\" is not defined."; - } - - static function undefinedVarByOpMessage($varName, $opName) - { - return "Variable \"$$varName\" is not defined by operation \"$opName\"."; + return $opName + ? "Variable \"$$varName\" is not defined by operation \"$opName\"." + : "Variable \"$$varName\" is not defined."; } public function __invoke(ValidationContext $context) { - $operation = null; - $visitedFragmentNames = []; - $definedVariableNames = []; + $variableNameDefined = []; return [ - // Visit FragmentDefinition after visiting FragmentSpread - 'visitSpreadFragments' => true, + Node::OPERATION_DEFINITION => [ + 'enter' => function() use (&$variableNameDefined) { + $variableNameDefined = []; + }, + 'leave' => function(OperationDefinition $operation) use (&$variableNameDefined, $context) { + $usages = $context->getRecursiveVariableUsages($operation); - Node::OPERATION_DEFINITION => function(OperationDefinition $node, $key, $parent, $path, $ancestors) use (&$operation, &$visitedFragmentNames, &$definedVariableNames) { - $operation = $node; - $visitedFragmentNames = []; - $definedVariableNames = []; - }, - Node::VARIABLE_DEFINITION => function(VariableDefinition $def) use (&$definedVariableNames) { - $definedVariableNames[$def->variable->name->value] = true; - }, - Node::VARIABLE => function(Variable $variable, $key, $parent, $path, $ancestors) use (&$definedVariableNames, &$visitedFragmentNames, &$operation) { - $varName = $variable->name->value; - if (empty($definedVariableNames[$varName])) { - $withinFragment = false; - foreach ($ancestors as $ancestor) { - if ($ancestor instanceof FragmentDefinition) { - $withinFragment = true; - break; + foreach ($usages as $usage) { + $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 ($withinFragment && $operation && $operation->name) { - return new Error( - self::undefinedVarByOpMessage($varName, $operation->name->value), - [$variable, $operation] - ); - } - return new Error( - self::undefinedVarMessage($varName), - [$variable] - ); } - }, - Node::FRAGMENT_SPREAD => function(FragmentSpread $spreadAST) use (&$visitedFragmentNames) { - // Only visit fragments of a particular name once per operation - if (!empty($visitedFragmentNames[$spreadAST->name->value])) { - return Visitor::skipNode(); - } - $visitedFragmentNames[$spreadAST->name->value] = true; + ], + Node::VARIABLE_DEFINITION => function(VariableDefinition $def) use (&$variableNameDefined) { + $variableNameDefined[$def->variable->name->value] = true; } ]; } diff --git a/src/Validator/Rules/NoUnusedFragments.php b/src/Validator/Rules/NoUnusedFragments.php index bea8422..bf88240 100644 --- a/src/Validator/Rules/NoUnusedFragments.php +++ b/src/Validator/Rules/NoUnusedFragments.php @@ -6,6 +6,7 @@ use GraphQL\Error; use GraphQL\Language\AST\FragmentDefinition; use GraphQL\Language\AST\FragmentSpread; use GraphQL\Language\AST\Node; +use GraphQL\Language\Visitor; use GraphQL\Validator\Messages; use GraphQL\Validator\ValidationContext; @@ -16,63 +17,45 @@ class NoUnusedFragments return "Fragment \"$fragName\" is never used."; } + public $operationDefs; + + public $fragmentDefs; + public function __invoke(ValidationContext $context) { - $fragmentDefs = []; - $spreadsWithinOperation = []; - $fragAdjacencies = new \stdClass(); - $spreadNames = new \stdClass(); + $this->operationDefs = []; + $this->fragmentDefs = []; return [ - Node::OPERATION_DEFINITION => function() use (&$spreadNames, &$spreadsWithinOperation) { - $spreadNames = new \stdClass(); - $spreadsWithinOperation[] = $spreadNames; + Node::OPERATION_DEFINITION => function($node) { + $this->operationDefs[] = $node; + return Visitor::skipNode(); }, - Node::FRAGMENT_DEFINITION => function(FragmentDefinition $def) use (&$fragmentDefs, &$spreadNames, &$fragAdjacencies) { - $fragmentDefs[] = $def; - $spreadNames = new \stdClass(); - $fragAdjacencies->{$def->name->value} = $spreadNames; - }, - Node::FRAGMENT_SPREAD => function(FragmentSpread $spread) use (&$spreadNames) { - $spreadNames->{$spread->name->value} = true; + Node::FRAGMENT_DEFINITION => function(FragmentDefinition $def) { + $this->fragmentDefs[] = $def; + return Visitor::skipNode(); }, Node::DOCUMENT => [ - 'leave' => function() use (&$fragAdjacencies, &$spreadsWithinOperation, &$fragmentDefs) { + 'leave' => function() use ($context) { $fragmentNameUsed = []; - foreach ($spreadsWithinOperation as $spreads) { - $this->reduceSpreadFragments($spreads, $fragmentNameUsed, $fragAdjacencies); - } - - $errors = []; - foreach ($fragmentDefs as $def) { - if (empty($fragmentNameUsed[$def->name->value])) { - $errors[] = new Error( - self::unusedFragMessage($def->name->value), - [$def] - ); + foreach ($this->operationDefs as $operation) { + foreach ($context->getRecursivelyReferencedFragments($operation) as $fragment) { + $fragmentNameUsed[$fragment->name->value] = true; + } + } + + foreach ($this->fragmentDefs as $fragmentDef) { + $fragName = $fragmentDef->name->value; + if (empty($fragmentNameUsed[$fragName])) { + $context->reportError(new Error( + self::unusedFragMessage($fragName), + [ $fragmentDef ] + )); } } - return !empty($errors) ? $errors : null; } ] ]; } - - private function reduceSpreadFragments($spreads, &$fragmentNameUsed, &$fragAdjacencies) - { - foreach ($spreads as $fragName => $fragment) { - if (empty($fragmentNameUsed[$fragName])) { - $fragmentNameUsed[$fragName] = true; - - if (isset($fragAdjacencies->{$fragName})) { - $this->reduceSpreadFragments( - $fragAdjacencies->{$fragName}, - $fragmentNameUsed, - $fragAdjacencies - ); - } - } - } - } } diff --git a/src/Validator/Rules/OverlappingFieldsCanBeMerged.php b/src/Validator/Rules/OverlappingFieldsCanBeMerged.php index fb27759..f9194ac 100644 --- a/src/Validator/Rules/OverlappingFieldsCanBeMerged.php +++ b/src/Validator/Rules/OverlappingFieldsCanBeMerged.php @@ -54,18 +54,15 @@ class OverlappingFieldsCanBeMerged $conflicts = $this->findConflicts($fieldMap, $context, $comparedSet); - if (!empty($conflicts)) { - return array_map(function ($conflict) { - $responseName = $conflict[0][0]; - $reason = $conflict[0][1]; - $fields = $conflict[1]; - - return new Error( - self::fieldsConflictMessage($responseName, $reason), - $fields - ); - }, $conflicts); + foreach ($conflicts as $conflict) { + $responseName = $conflict[0][0]; + $reason = $conflict[0][1]; + $fields = $conflict[1]; + $context->reportError(new Error( + self::fieldsConflictMessage($responseName, $reason), + $fields + )); } } ] diff --git a/src/Validator/Rules/PossibleFragmentSpreads.php b/src/Validator/Rules/PossibleFragmentSpreads.php index e4fcce0..2062138 100644 --- a/src/Validator/Rules/PossibleFragmentSpreads.php +++ b/src/Validator/Rules/PossibleFragmentSpreads.php @@ -32,10 +32,10 @@ class PossibleFragmentSpreads $fragType = Type::getNamedType($context->getType()); $parentType = $context->getParentType(); if ($fragType && $parentType && !$this->doTypesOverlap($fragType, $parentType)) { - return new Error( + $context->reportError(new Error( self::typeIncompatibleAnonSpreadMessage($parentType, $fragType), [$node] - ); + )); } }, Node::FRAGMENT_SPREAD => function(FragmentSpread $node) use ($context) { @@ -44,10 +44,10 @@ class PossibleFragmentSpreads $parentType = $context->getParentType(); if ($fragType && $parentType && !$this->doTypesOverlap($fragType, $parentType)) { - return new Error( + $context->reportError(new Error( self::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType), [$node] - ); + )); } } ]; diff --git a/src/Validator/Rules/ProvidedNonNullArguments.php b/src/Validator/Rules/ProvidedNonNullArguments.php index f8595a0..1cdc7fc 100644 --- a/src/Validator/Rules/ProvidedNonNullArguments.php +++ b/src/Validator/Rules/ProvidedNonNullArguments.php @@ -33,7 +33,6 @@ class ProvidedNonNullArguments if (!$fieldDef) { return Visitor::skipNode(); } - $errors = []; $argASTs = $fieldAST->arguments ?: []; $argASTMap = []; @@ -43,16 +42,12 @@ class ProvidedNonNullArguments foreach ($fieldDef->args as $argDef) { $argAST = isset($argASTMap[$argDef->name]) ? $argASTMap[$argDef->name] : null; if (!$argAST && $argDef->getType() instanceof NonNull) { - $errors[] = new Error( + $context->reportError(new Error( self::missingFieldArgMessage($fieldAST->name->value, $argDef->name, $argDef->getType()), [$fieldAST] - ); + )); } } - - if (!empty($errors)) { - return $errors; - } } ], Node::DIRECTIVE => [ @@ -61,7 +56,6 @@ class ProvidedNonNullArguments if (!$directiveDef) { return Visitor::skipNode(); } - $errors = []; $argASTs = $directiveAST->arguments ?: []; $argASTMap = []; foreach ($argASTs as $argAST) { @@ -71,15 +65,12 @@ class ProvidedNonNullArguments foreach ($directiveDef->args as $argDef) { $argAST = isset($argASTMap[$argDef->name]) ? $argASTMap[$argDef->name] : null; if (!$argAST && $argDef->getType() instanceof NonNull) { - $errors[] = new Error( + $context->reportError(new Error( self::missingDirectiveArgMessage($directiveAST->name->value, $argDef->name, $argDef->getType()), [$directiveAST] - ); + )); } } - if (!empty($errors)) { - return $errors; - } } ] ]; diff --git a/src/Validator/Rules/ScalarLeafs.php b/src/Validator/Rules/ScalarLeafs.php index 58238bd..f782af2 100644 --- a/src/Validator/Rules/ScalarLeafs.php +++ b/src/Validator/Rules/ScalarLeafs.php @@ -29,16 +29,16 @@ class ScalarLeafs if ($type) { if (Type::isLeafType($type)) { if ($node->selectionSet) { - return new Error( + $context->reportError(new Error( self::noSubselectionAllowedMessage($node->name->value, $type), [$node->selectionSet] - ); + )); } } else if (!$node->selectionSet) { - return new Error( + $context->reportError(new Error( self::requiredSubselectionMessage($node->name->value, $type), [$node] - ); + )); } } } diff --git a/src/Validator/Rules/VariablesAreInputTypes.php b/src/Validator/Rules/VariablesAreInputTypes.php index faca0ce..edc8c8f 100644 --- a/src/Validator/Rules/VariablesAreInputTypes.php +++ b/src/Validator/Rules/VariablesAreInputTypes.php @@ -27,10 +27,10 @@ class VariablesAreInputTypes // If the variable type is not an input type, return an error. if ($type && !Type::isInputType($type)) { $variableName = $node->variable->name->value; - return new Error( + $context->reportError(new Error( self::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)), [ $node->type ] - ); + )); } } ]; diff --git a/src/Validator/Rules/VariablesInAllowedPosition.php b/src/Validator/Rules/VariablesInAllowedPosition.php index ad6ccd7..9693161 100644 --- a/src/Validator/Rules/VariablesInAllowedPosition.php +++ b/src/Validator/Rules/VariablesInAllowedPosition.php @@ -47,10 +47,10 @@ class VariablesInAllowedPosition if ($varType && $inputType && !$this->varTypeAllowedForType($this->effectiveType($varType, $varDef), $inputType) ) { - return new Error( + $context->reportError(new Error( Messages::badVarPosMessage($varName, $varType, $inputType), [$variableAST] - ); + )); } } ]; diff --git a/src/Validator/ValidationContext.php b/src/Validator/ValidationContext.php index 27d56f1..ed2b4b0 100644 --- a/src/Validator/ValidationContext.php +++ b/src/Validator/ValidationContext.php @@ -1,5 +1,13 @@ */ private $_fragments; + /** + * @var SplObjectStorage + */ + private $_fragmentSpreads; + + /** + * @var SplObjectStorage + */ + private $_recursivelyReferencedFragments; + + /** + * @var SplObjectStorage + */ + private $_variableUsages; + + /** + * @var SplObjectStorage + */ + private $_recursiveVariableUsages; + + /** + * ValidationContext constructor. + * + * @param Schema $schema + * @param Document $ast + * @param TypeInfo $typeInfo + */ function __construct(Schema $schema, Document $ast, TypeInfo $typeInfo) { $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(); + } + + /** + * @param Error $error + */ + function reportError(Error $error) + { + $this->_errors[] = $error; + } + + /** + * @return Error[] + */ + function getErrors() + { + return $this->_errors; } /** @@ -80,6 +141,113 @@ class ValidationContext return isset($fragments[$name]) ? $fragments[$name] : null; } + /** + * @param HasSelectionSet $node + * @return FragmentSpread[] + */ + 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 === Node::FRAGMENT_SPREAD) { + $spreads[] = $selection; + } else if ($selection->selectionSet) { + $setsToVisit[] = $selection->selectionSet; + } + } + } + $this->_fragmentSpreads[$node] = $spreads; + } + return $spreads; + } + + /** + * @param OperationDefinition $operation + * @return FragmentDefinition[] + */ + function getRecursivelyReferencedFragments(OperationDefinition $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' => Variable, '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, [ + Node::VARIABLE_DEFINITION => function () { + return false; + }, + Node::VARIABLE => function (Variable $variable) use (&$newUsages, $typeInfo) { + $newUsages[] = ['node' => $variable, 'type' => $typeInfo->getInputType()]; + } + ])); + $usages = $newUsages; + $this->_variableUsages[$node] = $usages; + } + return $usages; + } + + /** + * @param OperationDefinition $operation + * @return array List of ['node' => Variable, 'type' => ?InputObjectType] + */ + function getRecursiveVariableUsages(OperationDefinition $operation) + { + $usages = isset($this->_recursiveVariableUsages[$operation]) ? $this->_recursiveVariableUsages[$operation] : null; + + if (!$usages) { + $usages = $this->getVariableUsages($operation); + $fragments = $this->getRecursivelyReferencedFragments($operation); + + $tmp = [$usages]; + for ($i = 0; $i < count($fragments); $i++) { + $tmp[] = $this->getVariableUsages($fragments[$i]); + } + $usages = call_user_func_array('array_merge', $tmp); + $this->_recursiveVariableUsages[$operation] = $usages; + } + return $usages; + } + /** * Returns OutputType * diff --git a/tests/Validator/ArgumentsOfCorrectTypeTest.php b/tests/Validator/ArgumentsOfCorrectTypeTest.php index 577be7c..f53d14a 100644 --- a/tests/Validator/ArgumentsOfCorrectTypeTest.php +++ b/tests/Validator/ArgumentsOfCorrectTypeTest.php @@ -7,25 +7,22 @@ use GraphQL\Validator\Rules\ArgumentsOfCorrectType; class ArgumentsOfCorrectTypeTest extends TestCase { - function missingArg($fieldName, $argName, $typeName, $line, $column) + function badValue($argName, $typeName, $value, $line, $column, $errors = null) { - return FormattedError::create( - Messages::missingArgMessage($fieldName, $argName, $typeName), - [new SourceLocation($line, $column)] - ); - } + $realErrors = !$errors ? ["Expected type \"$typeName\", found $value."] : $errors; - function badValue($argName, $typeName, $value, $line, $column) - { return FormattedError::create( - ArgumentsOfCorrectType::badValueMessage($argName, $typeName, $value), + ArgumentsOfCorrectType::badValueMessage($argName, $typeName, $value, $realErrors), [new SourceLocation($line, $column)] ); } // Validate: Argument values of correct type - // Valid values: + // Valid values + /** + * @it Good int value + */ public function testGoodIntValue() { $this->expectPassesRule(new ArgumentsOfCorrectType(), ' @@ -37,6 +34,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it Good boolean value + */ public function testGoodBooleanValue() { $this->expectPassesRule(new ArgumentsOfCorrectType(), ' @@ -48,6 +48,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it Good string value + */ public function testGoodStringValue() { $this->expectPassesRule(new ArgumentsOfCorrectType(), ' @@ -59,6 +62,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it Good float value + */ public function testGoodFloatValue() { $this->expectPassesRule(new ArgumentsOfCorrectType(), ' @@ -70,6 +76,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it Int into Float + */ public function testIntIntoFloat() { $this->expectPassesRule(new ArgumentsOfCorrectType(), ' @@ -81,6 +90,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it Int into ID + */ public function testIntIntoID() { $this->expectPassesRule(new ArgumentsOfCorrectType(), ' @@ -92,6 +104,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it String into ID + */ public function testStringIntoID() { $this->expectPassesRule(new ArgumentsOfCorrectType(), ' @@ -103,6 +118,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it Good enum value + */ public function testGoodEnumValue() { $this->expectPassesRule(new ArgumentsOfCorrectType(), ' @@ -115,6 +133,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase } // Invalid String values + + /** + * @it Int into String + */ public function testIntIntoString() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -128,6 +150,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } + /** + * @it Float into String + */ public function testFloatIntoString() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -141,6 +166,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } + /** + * @it Boolean into String + */ public function testBooleanIntoString() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -154,6 +182,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } + /** + * @it Unquoted String into String + */ public function testUnquotedStringIntoString() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -168,6 +199,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase } // Invalid Int values + + /** + * @it String into Int + */ public function testStringIntoInt() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -181,6 +216,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } + /** + * @it Big Int into Int + */ public function testBigIntIntoInt() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -194,6 +232,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } + /** + * @it Unquoted String into Int + */ public function testUnquotedStringIntoInt() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -207,6 +248,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } + /** + * @it Simple Float into Int + */ public function testSimpleFloatIntoInt() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -220,6 +264,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } + /** + * @it Float into Int + */ public function testFloatIntoInt() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -234,6 +281,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase } // Invalid Float values + + /** + * @it String into Float + */ public function testStringIntoFloat() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -247,6 +298,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } + /** + * @it Boolean into Float + */ public function testBooleanIntoFloat() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -260,6 +314,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } + /** + * @it Unquoted into Float + */ public function testUnquotedIntoFloat() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -274,6 +331,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase } // Invalid Boolean value + + /** + * @it Int into Boolean + */ public function testIntIntoBoolean() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -287,6 +348,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } + /** + * @it Float into Boolean + */ public function testFloatIntoBoolean() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -300,6 +364,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } + /** + * @it String into Boolean + */ public function testStringIntoBoolean() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -313,6 +380,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } + /** + * @it Unquoted into Boolean + */ public function testUnquotedIntoBoolean() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -327,6 +397,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase } // Invalid ID value + + /** + * @it Float into ID + */ public function testFloatIntoID() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -340,6 +414,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } + /** + * @it Boolean into ID + */ public function testBooleanIntoID() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -353,6 +430,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } + /** + * @it Unquoted into ID + */ public function testUnquotedIntoID() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -367,6 +447,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase } // Invalid Enum value + + /** + * @it Int into Enum + */ public function testIntIntoEnum() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -380,6 +464,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } + /** + * @it Float into Enum + */ public function testFloatIntoEnum() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -393,6 +480,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } + /** + * @it String into Enum + */ public function testStringIntoEnum() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -406,6 +496,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } + /** + * @it Boolean into Enum + */ public function testBooleanIntoEnum() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -419,6 +512,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } + /** + * @it Unknown Enum Value into Enum + */ public function testUnknownEnumValueIntoEnum() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -432,6 +528,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase ]); } + /** + * @it Different case Enum Value into Enum + */ public function testDifferentCaseEnumValueIntoEnum() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -446,6 +545,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase } // Valid List value + + /** + * @it Good list value + */ public function testGoodListValue() { $this->expectPassesRule(new ArgumentsOfCorrectType(), ' @@ -457,6 +560,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it Empty list value + */ public function testEmptyListValue() { $this->expectPassesRule(new ArgumentsOfCorrectType(), ' @@ -468,6 +574,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it Single value into List + */ public function testSingleValueIntoList() { $this->expectPassesRule(new ArgumentsOfCorrectType(), ' @@ -480,6 +589,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase } // Invalid List value + + /** + * @it Incorrect item type + */ public function testIncorrectItemtype() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -489,10 +602,15 @@ class ArgumentsOfCorrectTypeTest extends TestCase } } ', [ - $this->badValue('stringListArg', '[String]', '["one", 2]', 4, 47), + $this->badValue('stringListArg', '[String]', '["one", 2]', 4, 47, [ + 'In element #1: Expected type "String", found 2.' + ]), ]); } + /** + * @it Single value of incorrect type + */ public function testSingleValueOfIncorrectType() { $this->expectFailsRule(new ArgumentsOfCorrectType(), ' @@ -502,11 +620,15 @@ class ArgumentsOfCorrectTypeTest extends TestCase } } ', [ - $this->badValue('stringListArg', '[String]', '1', 4, 47), + $this->badValue('stringListArg', 'String', '1', 4, 47), ]); } // Valid non-nullable value + + /** + * @it Arg on optional arg + */ public function testArgOnOptionalArg() { $this->expectPassesRule(new ArgumentsOfCorrectType(), ' @@ -518,6 +640,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it No Arg on optional arg + */ public function testNoArgOnOptionalArg() { $this->expectPassesRule(new ArgumentsOfCorrectType(), ' @@ -529,6 +654,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it Multiple args + */ public function testMultipleArgs() { $this->expectPassesRule(new ArgumentsOfCorrectType(), ' @@ -540,6 +668,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it Multiple args reverse order + */ public function testMultipleArgsReverseOrder() { $this->expectPassesRule(new ArgumentsOfCorrectType(), ' @@ -551,6 +682,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it No args on multiple optional + */ public function testNoArgsOnMultipleOptional() { $this->expectPassesRule(new ArgumentsOfCorrectType(), ' @@ -562,6 +696,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it One arg on multiple optional + */ public function testOneArgOnMultipleOptional() { $this->expectPassesRule(new ArgumentsOfCorrectType, ' @@ -573,6 +710,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it Second arg on multiple optional + */ public function testSecondArgOnMultipleOptional() { $this->expectPassesRule(new ArgumentsOfCorrectType, ' @@ -584,6 +724,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it Multiple reqs on mixedList + */ public function testMultipleReqsOnMixedList() { $this->expectPassesRule(new ArgumentsOfCorrectType, ' @@ -595,6 +738,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it Multiple reqs and one opt on mixedList + */ public function testMultipleReqsAndOneOptOnMixedList() { $this->expectPassesRule(new ArgumentsOfCorrectType, ' @@ -606,6 +752,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it All reqs and opts on mixedList + */ public function testAllReqsAndOptsOnMixedList() { $this->expectPassesRule(new ArgumentsOfCorrectType, ' @@ -618,6 +767,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase } // Invalid non-nullable value + + /** + * @it Incorrect value type + */ public function testIncorrectValueType() { $this->expectFailsRule(new ArgumentsOfCorrectType, ' @@ -627,11 +780,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase } } ', [ - $this->badValue('req2', 'Int!', '"two"', 4, 32), - $this->badValue('req1', 'Int!', '"one"', 4, 45), + $this->badValue('req2', 'Int', '"two"', 4, 32), + $this->badValue('req1', 'Int', '"one"', 4, 45), ]); } + /** + * @it Incorrect value and missing argument + */ public function testIncorrectValueAndMissingArgument() { $this->expectFailsRule(new ArgumentsOfCorrectType, ' @@ -641,12 +797,16 @@ class ArgumentsOfCorrectTypeTest extends TestCase } } ', [ - $this->badValue('req1', 'Int!', '"one"', 4, 32), + $this->badValue('req1', 'Int', '"one"', 4, 32), ]); } // Valid input object value + + /** + * @it Optional arg, despite required field in type + */ public function testOptionalArgDespiteRequiredFieldInType() { $this->expectPassesRule(new ArgumentsOfCorrectType, ' @@ -658,6 +818,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it Partial object, only required + */ public function testPartialObjectOnlyRequired() { $this->expectPassesRule(new ArgumentsOfCorrectType, ' @@ -669,6 +832,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it Partial object, required field can be falsey + */ public function testPartialObjectRequiredFieldCanBeFalsey() { $this->expectPassesRule(new ArgumentsOfCorrectType, ' @@ -680,6 +846,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it Partial object, including required + */ public function testPartialObjectIncludingRequired() { $this->expectPassesRule(new ArgumentsOfCorrectType, ' @@ -691,6 +860,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it Full object + */ public function testFullObject() { $this->expectPassesRule(new ArgumentsOfCorrectType, ' @@ -708,6 +880,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase '); } + /** + * @it Full object with fields in different order + */ public function testFullObjectWithFieldsInDifferentOrder() { $this->expectPassesRule(new ArgumentsOfCorrectType(), ' @@ -726,6 +901,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase } // Invalid input object value + + /** + * @it Partial object, missing required + */ public function testPartialObjectMissingRequired() { $this->expectFailsRule(new ArgumentsOfCorrectType, ' @@ -735,10 +914,15 @@ class ArgumentsOfCorrectTypeTest extends TestCase } } ', [ - $this->badValue('complexArg', 'ComplexInput', '{intField: 4}', 4, 41), + $this->badValue('complexArg', 'ComplexInput', '{intField: 4}', 4, 41, [ + 'In field "requiredField": Expected "Boolean!", found null.' + ]), ]); } + /** + * @it Partial object, invalid field type + */ public function testPartialObjectInvalidFieldType() { $this->expectFailsRule(new ArgumentsOfCorrectType, ' @@ -756,11 +940,15 @@ class ArgumentsOfCorrectTypeTest extends TestCase 'ComplexInput', '{stringListField: ["one", 2], requiredField: true}', 4, - 41 + 41, + [ 'In field "stringListField": In element #1: Expected type "String", found 2.' ] ), ]); } + /** + * @it Partial object, unknown field arg + */ public function testPartialObjectUnknownFieldArg() { $this->expectFailsRule(new ArgumentsOfCorrectType, ' @@ -778,8 +966,45 @@ class ArgumentsOfCorrectTypeTest extends TestCase 'ComplexInput', '{requiredField: true, unknownField: "value"}', 4, - 41 + 41, + [ 'In field "unknownField": Unknown field.' ] ), ]); } + + // Directive arguments + + /** + * @it with directives of valid types + */ + public function testWithDirectivesOfValidTypes() + { + $this->expectPassesRule(new ArgumentsOfCorrectType(), ' + { + dog @include(if: true) { + name + } + human @skip(if: false) { + name + } + } + '); + } + + /** + * @it with directive with incorrect types + */ + public function testWithDirectiveWithIncorrectTypes() + { + $this->expectFailsRule(new ArgumentsOfCorrectType, ' + { + dog @include(if: "yes") { + name @skip(if: ENUM) + } + } + ', [ + $this->badValue('if', 'Boolean', '"yes"', 3, 28), + $this->badValue('if', 'Boolean', 'ENUM', 4, 28), + ]); + } } diff --git a/tests/Validator/DefaultValuesOfCorrectTypeTest.php b/tests/Validator/DefaultValuesOfCorrectTypeTest.php index f48de76..a979cdd 100644 --- a/tests/Validator/DefaultValuesOfCorrectTypeTest.php +++ b/tests/Validator/DefaultValuesOfCorrectTypeTest.php @@ -10,6 +10,9 @@ class DefaultValuesOfCorrectTypeTest extends TestCase { // Validate: Variable default values of correct type + /** + * @it variables with no default values + */ public function testVariablesWithNoDefaultValues() { $this->expectPassesRule(new DefaultValuesOfCorrectType, ' @@ -19,6 +22,9 @@ class DefaultValuesOfCorrectTypeTest extends TestCase '); } + /** + * @it required variables without default values + */ public function testRequiredVariablesWithoutDefaultValues() { $this->expectPassesRule(new DefaultValuesOfCorrectType, ' @@ -28,6 +34,9 @@ class DefaultValuesOfCorrectTypeTest extends TestCase '); } + /** + * @it variables with valid default values + */ public function testVariablesWithValidDefaultValues() { $this->expectPassesRule(new DefaultValuesOfCorrectType, ' @@ -41,6 +50,9 @@ class DefaultValuesOfCorrectTypeTest extends TestCase '); } + /** + * @it no required variables with default values + */ public function testNoRequiredVariablesWithDefaultValues() { $this->expectFailsRule(new DefaultValuesOfCorrectType, ' @@ -53,6 +65,9 @@ class DefaultValuesOfCorrectTypeTest extends TestCase ]); } + /** + * @it variables with invalid default values + */ public function testVariablesWithInvalidDefaultValues() { $this->expectFailsRule(new DefaultValuesOfCorrectType, ' @@ -64,12 +79,21 @@ class DefaultValuesOfCorrectTypeTest extends TestCase dog { name } } ', [ - $this->badValue('a', 'Int', '"one"', 3, 19), - $this->badValue('b', 'String', '4', 4, 22), - $this->badValue('c', 'ComplexInput', '"notverycomplex"', 5, 28) + $this->badValue('a', 'Int', '"one"', 3, 19, [ + 'Expected type "Int", found "one".' + ]), + $this->badValue('b', 'String', '4', 4, 22, [ + 'Expected type "String", found 4.' + ]), + $this->badValue('c', 'ComplexInput', '"notverycomplex"', 5, 28, [ + 'Expected "ComplexInput", found not an object.' + ]) ]); } + /** + * @it complex variables missing required field + */ public function testComplexVariablesMissingRequiredField() { $this->expectFailsRule(new DefaultValuesOfCorrectType, ' @@ -77,10 +101,15 @@ class DefaultValuesOfCorrectTypeTest extends TestCase dog { name } } ', [ - $this->badValue('a', 'ComplexInput', '{intField: 3}', 2, 53) + $this->badValue('a', 'ComplexInput', '{intField: 3}', 2, 53, [ + 'In field "requiredField": Expected "Boolean!", found null.' + ]) ]); } + /** + * @it list variables with invalid item + */ public function testListVariablesWithInvalidItem() { $this->expectFailsRule(new DefaultValuesOfCorrectType, ' @@ -88,22 +117,26 @@ class DefaultValuesOfCorrectTypeTest extends TestCase dog { name } } ', [ - $this->badValue('a', '[String]', '["one", 2]', 2, 40) + $this->badValue('a', '[String]', '["one", 2]', 2, 40, [ + 'In element #1: Expected type "String", found 2.' + ]) ]); } private function defaultForNonNullArg($varName, $typeName, $guessTypeName, $line, $column) { return FormattedError::create( - Messages::defaultForNonNullArgMessage($varName, $typeName, $guessTypeName), + DefaultValuesOfCorrectType::defaultForNonNullArgMessage($varName, $typeName, $guessTypeName), [ new SourceLocation($line, $column) ] ); } - private function badValue($varName, $typeName, $val, $line, $column) + private function badValue($varName, $typeName, $val, $line, $column, $errors = null) { + $realErrors = !$errors ? ["Expected type \"$typeName\", found $val."] : $errors; + return FormattedError::create( - Messages::badValueForDefaultArgMessage($varName, $typeName, $val), + DefaultValuesOfCorrectType::badValueForDefaultArgMessage($varName, $typeName, $val, $realErrors), [ new SourceLocation($line, $column) ] ); } diff --git a/tests/Validator/FieldsOnCorrectTypeTest.php b/tests/Validator/FieldsOnCorrectTypeTest.php index ac5543a..09d4ce8 100644 --- a/tests/Validator/FieldsOnCorrectTypeTest.php +++ b/tests/Validator/FieldsOnCorrectTypeTest.php @@ -9,6 +9,10 @@ use GraphQL\Validator\Rules\FieldsOnCorrectType; class FieldsOnCorrectTypeTest extends TestCase { // Validate: Fields on correct type + + /** + * @it Object field selection + */ public function testObjectFieldSelection() { $this->expectPassesRule(new FieldsOnCorrectType(), ' @@ -19,6 +23,9 @@ class FieldsOnCorrectTypeTest extends TestCase '); } + /** + * @it Aliased object field selection + */ public function testAliasedObjectFieldSelection() { $this->expectPassesRule(new FieldsOnCorrectType, ' @@ -29,6 +36,9 @@ class FieldsOnCorrectTypeTest extends TestCase '); } + /** + * @it Interface field selection + */ public function testInterfaceFieldSelection() { $this->expectPassesRule(new FieldsOnCorrectType, ' @@ -39,6 +49,9 @@ class FieldsOnCorrectTypeTest extends TestCase '); } + /** + * @it Aliased interface field selection + */ public function testAliasedInterfaceFieldSelection() { $this->expectPassesRule(new FieldsOnCorrectType, ' @@ -48,6 +61,9 @@ class FieldsOnCorrectTypeTest extends TestCase '); } + /** + * @it Lying alias selection + */ public function testLyingAliasSelection() { $this->expectPassesRule(new FieldsOnCorrectType, ' @@ -57,6 +73,9 @@ class FieldsOnCorrectTypeTest extends TestCase '); } + /** + * @it Ignores fields on unknown type + */ public function testIgnoresFieldsOnUnknownType() { $this->expectPassesRule(new FieldsOnCorrectType, ' @@ -66,17 +85,41 @@ class FieldsOnCorrectTypeTest extends TestCase '); } + /** + * @it reports errors when type is known again + */ + public function testReportsErrorsWhenTypeIsKnownAgain() + { + $this->expectFailsRule(new FieldsOnCorrectType, ' + fragment typeKnownAgain on Pet { + unknown_pet_field { + ... on Cat { + unknown_cat_field + } + } + }', + [ $this->undefinedField('unknown_pet_field', 'Pet', [], 3, 9), + $this->undefinedField('unknown_cat_field', 'Cat', [], 5, 13) ] + ); + } + + /** + * @it Field not defined on fragment + */ public function testFieldNotDefinedOnFragment() { $this->expectFailsRule(new FieldsOnCorrectType, ' fragment fieldNotDefined on Dog { meowVolume }', - [$this->undefinedField('meowVolume', 'Dog', 3, 9)] + [$this->undefinedField('meowVolume', 'Dog', [], 3, 9)] ); } - public function testFieldNotDefinedDeeplyOnlyReportsFirst() + /** + * @it Ignores deeply unknown field + */ + public function testIgnoresDeeplyUnknownField() { $this->expectFailsRule(new FieldsOnCorrectType, ' fragment deepFieldNotDefined on Dog { @@ -84,10 +127,13 @@ class FieldsOnCorrectTypeTest extends TestCase deeper_unknown_field } }', - [$this->undefinedField('unknown_field', 'Dog', 3, 9)] + [$this->undefinedField('unknown_field', 'Dog', [], 3, 9)] ); } + /** + * @it Sub-field not defined + */ public function testSubFieldNotDefined() { $this->expectFailsRule(new FieldsOnCorrectType, ' @@ -96,10 +142,13 @@ class FieldsOnCorrectTypeTest extends TestCase unknown_field } }', - [$this->undefinedField('unknown_field', 'Pet', 4, 11)] + [$this->undefinedField('unknown_field', 'Pet', [], 4, 11)] ); } + /** + * @it Field not defined on inline fragment + */ public function testFieldNotDefinedOnInlineFragment() { $this->expectFailsRule(new FieldsOnCorrectType, ' @@ -108,50 +157,65 @@ class FieldsOnCorrectTypeTest extends TestCase meowVolume } }', - [$this->undefinedField('meowVolume', 'Dog', 4, 11)] + [$this->undefinedField('meowVolume', 'Dog', [], 4, 11)] ); } + /** + * @it Aliased field target not defined + */ public function testAliasedFieldTargetNotDefined() { $this->expectFailsRule(new FieldsOnCorrectType, ' fragment aliasedFieldTargetNotDefined on Dog { volume : mooVolume }', - [$this->undefinedField('mooVolume', 'Dog', 3, 9)] + [$this->undefinedField('mooVolume', 'Dog', [], 3, 9)] ); } + /** + * @it Aliased lying field target not defined + */ public function testAliasedLyingFieldTargetNotDefined() { $this->expectFailsRule(new FieldsOnCorrectType, ' fragment aliasedLyingFieldTargetNotDefined on Dog { barkVolume : kawVolume }', - [$this->undefinedField('kawVolume', 'Dog', 3, 9)] + [$this->undefinedField('kawVolume', 'Dog', [], 3, 9)] ); } + /** + * @it Not defined on interface + */ public function testNotDefinedOnInterface() { $this->expectFailsRule(new FieldsOnCorrectType, ' fragment notDefinedOnInterface on Pet { tailLength }', - [$this->undefinedField('tailLength', 'Pet', 3, 9)] + [$this->undefinedField('tailLength', 'Pet', [], 3, 9)] ); } + /** + * @it Defined on implementors but not on interface + */ public function testDefinedOnImplmentorsButNotOnInterface() { $this->expectFailsRule(new FieldsOnCorrectType, ' fragment definedOnImplementorsButNotInterface on Pet { nickname }', - [$this->undefinedField('nickname', 'Pet', 3, 9)] + [$this->undefinedField('nickname', 'Pet', [ 'Cat', 'Dog' ], 3, 9)] ); } + /** + * @it Meta field selection on union + */ public function testMetaFieldSelectionOnUnion() { $this->expectPassesRule(new FieldsOnCorrectType, ' @@ -161,26 +225,35 @@ class FieldsOnCorrectTypeTest extends TestCase ); } + /** + * @it Direct field selection on union + */ public function testDirectFieldSelectionOnUnion() { $this->expectFailsRule(new FieldsOnCorrectType, ' fragment directFieldSelectionOnUnion on CatOrDog { directField }', - [$this->undefinedField('directField', 'CatOrDog', 3, 9)] + [$this->undefinedField('directField', 'CatOrDog', [], 3, 9)] ); } + /** + * @it Defined on implementors queried on union + */ public function testDefinedOnImplementorsQueriedOnUnion() { $this->expectFailsRule(new FieldsOnCorrectType, ' fragment definedOnImplementorsQueriedOnUnion on CatOrDog { name }', - [$this->undefinedField('name', 'CatOrDog', 3, 9)] + [$this->undefinedField('name', 'CatOrDog', [ 'Being', 'Pet', 'Canine', 'Cat', 'Dog' ], 3, 9)] ); } + /** + * @it valid field in inline fragment + */ public function testValidFieldInInlineFragment() { $this->expectPassesRule(new FieldsOnCorrectType, ' @@ -192,10 +265,45 @@ class FieldsOnCorrectTypeTest extends TestCase '); } - private function undefinedField($field, $type, $line, $column) + // Describe: Fields on correct type error message + + /** + * @it Works with no suggestions + */ + public function testWorksWithNoSuggestions() + { + $this->assertEquals('Cannot query field "T" on type "f".', FieldsOnCorrectType::undefinedFieldMessage('T', 'f', [])); + } + + /** + * @it Works with no small numbers of suggestions + */ + public function testWorksWithNoSmallNumbersOfSuggestions() + { + $expected = 'Cannot query field "T" on type "f". ' . + 'However, this field exists on "A", "B". ' . + 'Perhaps you meant to use an inline fragment?'; + + $this->assertEquals($expected, FieldsOnCorrectType::undefinedFieldMessage('T', 'f', [ 'A', 'B' ])); + } + + /** + * @it Works with lots of suggestions + */ + public function testWorksWithLotsOfSuggestions() + { + $expected = 'Cannot query field "T" on type "f". ' . + 'However, this field exists on "A", "B", "C", "D", "E", ' . + 'and 1 other types. ' . + 'Perhaps you meant to use an inline fragment?'; + + $this->assertEquals($expected, FieldsOnCorrectType::undefinedFieldMessage('T', 'f', [ 'A', 'B', 'C', 'D', 'E', 'F' ])); + } + + private function undefinedField($field, $type, $suggestions, $line, $column) { return FormattedError::create( - Messages::undefinedFieldMessage($field, $type), + FieldsOnCorrectType::undefinedFieldMessage($field, $type, $suggestions), [new SourceLocation($line, $column)] ); } diff --git a/tests/Validator/FragmentsOnCompositeTypesTest.php b/tests/Validator/FragmentsOnCompositeTypesTest.php index 4148bac..e1ad26e 100644 --- a/tests/Validator/FragmentsOnCompositeTypesTest.php +++ b/tests/Validator/FragmentsOnCompositeTypesTest.php @@ -9,6 +9,9 @@ class FragmentsOnCompositeTypesTest extends TestCase { // Validate: Fragments on composite types + /** + * @it object is valid fragment type + */ public function testObjectIsValidFragmentType() { $this->expectPassesRule(new FragmentsOnCompositeTypes, ' @@ -18,6 +21,9 @@ class FragmentsOnCompositeTypesTest extends TestCase '); } + /** + * @it interface is valid fragment type + */ public function testInterfaceIsValidFragmentType() { $this->expectPassesRule(new FragmentsOnCompositeTypes, ' @@ -27,6 +33,9 @@ class FragmentsOnCompositeTypesTest extends TestCase '); } + /** + * @it object is valid inline fragment type + */ public function testObjectIsValidInlineFragmentType() { $this->expectPassesRule(new FragmentsOnCompositeTypes, ' @@ -38,6 +47,23 @@ class FragmentsOnCompositeTypesTest extends TestCase '); } + /** + * @it inline fragment without type is valid + */ + public function testInlineFragmentWithoutTypeIsValid() + { + $this->expectPassesRule(new FragmentsOnCompositeTypes, ' + fragment validFragment on Pet { + ... { + name + } + } + '); + } + + /** + * @it union is valid fragment type + */ public function testUnionIsValidFragmentType() { $this->expectPassesRule(new FragmentsOnCompositeTypes, ' @@ -47,6 +73,9 @@ class FragmentsOnCompositeTypesTest extends TestCase '); } + /** + * @it scalar is invalid fragment type + */ public function testScalarIsInvalidFragmentType() { $this->expectFailsRule(new FragmentsOnCompositeTypes, ' @@ -57,6 +86,9 @@ class FragmentsOnCompositeTypesTest extends TestCase [$this->error('scalarFragment', 'Boolean', 2, 34)]); } + /** + * @it enum is invalid fragment type + */ public function testEnumIsInvalidFragmentType() { $this->expectFailsRule(new FragmentsOnCompositeTypes, ' @@ -67,6 +99,9 @@ class FragmentsOnCompositeTypesTest extends TestCase [$this->error('scalarFragment', 'FurColor', 2, 34)]); } + /** + * @it input object is invalid fragment type + */ public function testInputObjectIsInvalidFragmentType() { $this->expectFailsRule(new FragmentsOnCompositeTypes, ' @@ -77,6 +112,9 @@ class FragmentsOnCompositeTypesTest extends TestCase [$this->error('inputFragment', 'ComplexInput', 2, 33)]); } + /** + * @it scalar is invalid inline fragment type + */ public function testScalarIsInvalidInlineFragmentType() { $this->expectFailsRule(new FragmentsOnCompositeTypes, ' diff --git a/tests/Validator/KnownArgumentNamesTest.php b/tests/Validator/KnownArgumentNamesTest.php index dba1e2f..dece1a8 100644 --- a/tests/Validator/KnownArgumentNamesTest.php +++ b/tests/Validator/KnownArgumentNamesTest.php @@ -8,6 +8,10 @@ use GraphQL\Validator\Rules\KnownArgumentNames; class KnownArgumentNamesTest extends TestCase { // Validate: Known argument names: + + /** + * @it single arg is known + */ public function testSingleArgIsKnown() { $this->expectPassesRule(new KnownArgumentNames, ' @@ -17,6 +21,9 @@ class KnownArgumentNamesTest extends TestCase '); } + /** + * @it multiple args are known + */ public function testMultipleArgsAreKnown() { $this->expectPassesRule(new KnownArgumentNames, ' @@ -26,6 +33,9 @@ class KnownArgumentNamesTest extends TestCase '); } + /** + * @it ignores args of unknown fields + */ public function testIgnoresArgsOfUnknownFields() { $this->expectPassesRule(new KnownArgumentNames, ' @@ -35,6 +45,9 @@ class KnownArgumentNamesTest extends TestCase '); } + /** + * @it multiple args in reverse order are known + */ public function testMultipleArgsInReverseOrderAreKnown() { $this->expectPassesRule(new KnownArgumentNames, ' @@ -44,6 +57,9 @@ class KnownArgumentNamesTest extends TestCase '); } + /** + * @it no args on optional arg + */ public function testNoArgsOnOptionalArg() { $this->expectPassesRule(new KnownArgumentNames, ' @@ -53,6 +69,9 @@ class KnownArgumentNamesTest extends TestCase '); } + /** + * @it args are known deeply + */ public function testArgsAreKnownDeeply() { $this->expectPassesRule(new KnownArgumentNames, ' @@ -71,6 +90,9 @@ class KnownArgumentNamesTest extends TestCase '); } + /** + * @it directive args are known + */ public function testDirectiveArgsAreKnown() { $this->expectPassesRule(new KnownArgumentNames, ' @@ -80,6 +102,9 @@ class KnownArgumentNamesTest extends TestCase '); } + /** + * @it undirective args are invalid + */ public function testUndirectiveArgsAreInvalid() { $this->expectFailsRule(new KnownArgumentNames, ' @@ -91,6 +116,9 @@ class KnownArgumentNamesTest extends TestCase ]); } + /** + * @it invalid arg name + */ public function testInvalidArgName() { $this->expectFailsRule(new KnownArgumentNames, ' @@ -102,6 +130,9 @@ class KnownArgumentNamesTest extends TestCase ]); } + /** + * @it unknown args amongst known args + */ public function testUnknownArgsAmongstKnownArgs() { $this->expectFailsRule(new KnownArgumentNames, ' @@ -114,6 +145,9 @@ class KnownArgumentNamesTest extends TestCase ]); } + /** + * @it unknown args deeply + */ public function testUnknownArgsDeeply() { $this->expectFailsRule(new KnownArgumentNames, ' diff --git a/tests/Validator/KnownDirectivesTest.php b/tests/Validator/KnownDirectivesTest.php index 50ec3e4..990ab30 100644 --- a/tests/Validator/KnownDirectivesTest.php +++ b/tests/Validator/KnownDirectivesTest.php @@ -8,6 +8,10 @@ use GraphQL\Validator\Rules\KnownDirectives; class KnownDirectivesTest extends TestCase { // Validate: Known directives + + /** + * @it with no directives + */ public function testWithNoDirectives() { $this->expectPassesRule(new KnownDirectives, ' @@ -22,6 +26,9 @@ class KnownDirectivesTest extends TestCase '); } + /** + * @it with known directives + */ public function testWithKnownDirectives() { $this->expectPassesRule(new KnownDirectives, ' @@ -36,6 +43,9 @@ class KnownDirectivesTest extends TestCase '); } + /** + * @it with unknown directive + */ public function testWithUnknownDirective() { $this->expectFailsRule(new KnownDirectives, ' @@ -49,6 +59,9 @@ class KnownDirectivesTest extends TestCase ]); } + /** + * @it with many unknown directives + */ public function testWithManyUnknownDirectives() { $this->expectFailsRule(new KnownDirectives, ' @@ -70,6 +83,9 @@ class KnownDirectivesTest extends TestCase ]); } + /** + * @it with well placed directives + */ public function testWithWellPlacedDirectives() { $this->expectPassesRule(new KnownDirectives, ' @@ -82,15 +98,20 @@ class KnownDirectivesTest extends TestCase '); } + /** + * @it with misplaced directives + */ public function testWithMisplacedDirectives() { $this->expectFailsRule(new KnownDirectives, ' query Foo @include(if: true) { - name - ...Frag + name @operationOnly + ...Frag @operationOnly } ', [ - $this->misplacedDirective('include', 'operation', 2, 17) + $this->misplacedDirective('include', 'QUERY', 2, 17), + $this->misplacedDirective('operationOnly', 'FIELD', 3, 14), + $this->misplacedDirective('operationOnly', 'FRAGMENT_SPREAD', 4, 17), ]); } diff --git a/tests/Validator/KnownFragmentNamesTest.php b/tests/Validator/KnownFragmentNamesTest.php index 1a39ff1..36ba408 100644 --- a/tests/Validator/KnownFragmentNamesTest.php +++ b/tests/Validator/KnownFragmentNamesTest.php @@ -9,6 +9,9 @@ class KnownFragmentNamesTest extends TestCase { // Validate: Known fragment names + /** + * @it known fragment names are valid + */ public function testKnownFragmentNamesAreValid() { $this->expectPassesRule(new KnownFragmentNames, ' @@ -33,6 +36,9 @@ class KnownFragmentNamesTest extends TestCase '); } + /** + * @it unknown fragment names are invalid + */ public function testUnknownFragmentNamesAreInvalid() { $this->expectFailsRule(new KnownFragmentNames, ' diff --git a/tests/Validator/KnownTypeNamesTest.php b/tests/Validator/KnownTypeNamesTest.php index b9994ef..467b9dc 100644 --- a/tests/Validator/KnownTypeNamesTest.php +++ b/tests/Validator/KnownTypeNamesTest.php @@ -9,6 +9,9 @@ class KnownTypeNamesTest extends TestCase { // Validate: Known type names + /** + * @it known type names are valid + */ public function testKnownTypeNamesAreValid() { $this->expectPassesRule(new KnownTypeNames, ' @@ -23,6 +26,9 @@ class KnownTypeNamesTest extends TestCase '); } + /** + * @it unknown type names are invalid + */ public function testUnknownTypeNamesAreInvalid() { $this->expectFailsRule(new KnownTypeNames, ' @@ -42,6 +48,32 @@ class KnownTypeNamesTest extends TestCase ]); } + /** + * @it ignores type definitions + */ + public function testIgnoresTypeDefinitions() + { + $this->expectFailsRule(new KnownTypeNames, ' + type NotInTheSchema { + field: FooBar + } + interface FooBar { + field: NotInTheSchema + } + union U = A | B + input Blob { + field: UnknownType + } + query Foo($var: NotInTheSchema) { + user(id: $var) { + id + } + } + ', [ + $this->unknownType('NotInTheSchema', 12, 23), + ]); + } + private function unknownType($typeName, $line, $column) { return FormattedError::create( diff --git a/tests/Validator/LoneAnonymousOperationTest.php b/tests/Validator/LoneAnonymousOperationTest.php new file mode 100644 index 0000000..d0d5e72 --- /dev/null +++ b/tests/Validator/LoneAnonymousOperationTest.php @@ -0,0 +1,127 @@ +expectPassesRule(new LoneAnonymousOperation, ' + fragment fragA on Type { + field + } + '); + } + + /** + * @it one anon operation + */ + public function testOneAnonOperation() + { + $this->expectPassesRule(new LoneAnonymousOperation, ' + { + field + } + '); + } + + /** + * @it multiple named operations + */ + public function testMultipleNamedOperations() + { + $this->expectPassesRule(new LoneAnonymousOperation, ' + query Foo { + field + } + + query Bar { + field + } + '); + } + + /** + * @it anon operation with fragment + */ + public function testAnonOperationWithFragment() + { + $this->expectPassesRule(new LoneAnonymousOperation, ' + { + ...Foo + } + fragment Foo on Type { + field + } + '); + } + + /** + * @it multiple anon operations + */ + public function testMultipleAnonOperations() + { + $this->expectFailsRule(new LoneAnonymousOperation, ' + { + fieldA + } + { + fieldB + } + ', [ + $this->anonNotAlone(2, 7), + $this->anonNotAlone(5, 7) + ]); + } + + /** + * @it anon operation with a mutation + */ + public function testAnonOperationWithMutation() + { + $this->expectFailsRule(new LoneAnonymousOperation, ' + { + fieldA + } + mutation Foo { + fieldB + } + ', [ + $this->anonNotAlone(2, 7) + ]); + } + + /** + * @it anon operation with a subscription + */ + public function testAnonOperationWithSubscription() + { + $this->expectFailsRule(new LoneAnonymousOperation, ' + { + fieldA + } + subscription Foo { + fieldB + } + ', [ + $this->anonNotAlone(2, 7) + ]); + } + + private function anonNotAlone($line, $column) + { + return FormattedError::create( + LoneAnonymousOperation::anonOperationNotAloneMessage(), + [new SourceLocation($line, $column)] + ); + + } +} \ No newline at end of file diff --git a/tests/Validator/NoFragmentCyclesTest.php b/tests/Validator/NoFragmentCyclesTest.php index 8704bb3..cfb9aa5 100644 --- a/tests/Validator/NoFragmentCyclesTest.php +++ b/tests/Validator/NoFragmentCyclesTest.php @@ -9,6 +9,9 @@ class NoFragmentCyclesTest extends TestCase { // Validate: No circular fragment spreads + /** + * @it single reference is valid + */ public function testSingleReferenceIsValid() { $this->expectPassesRule(new NoFragmentCycles(), ' @@ -17,6 +20,9 @@ class NoFragmentCyclesTest extends TestCase '); } + /** + * @it spreading twice is not circular + */ public function testSpreadingTwiceIsNotCircular() { $this->expectPassesRule(new NoFragmentCycles, ' @@ -25,6 +31,9 @@ class NoFragmentCyclesTest extends TestCase '); } + /** + * @it spreading twice indirectly is not circular + */ public function testSpreadingTwiceIndirectlyIsNotCircular() { $this->expectPassesRule(new NoFragmentCycles, ' @@ -34,6 +43,9 @@ class NoFragmentCyclesTest extends TestCase '); } + /** + * @it double spread within abstract types + */ public function testDoubleSpreadWithinAbstractTypes() { $this->expectPassesRule(new NoFragmentCycles, ' @@ -49,6 +61,21 @@ class NoFragmentCyclesTest extends TestCase '); } + /** + * @it does not false positive on unknown fragment + */ + public function testDoesNotFalsePositiveOnUnknownFragment() + { + $this->expectPassesRule(new NoFragmentCycles, ' + fragment nameFragment on Pet { + ...UnknownFragment + } + '); + } + + /** + * @it spreading recursively within field fails + */ public function testSpreadingRecursivelyWithinFieldFails() { $this->expectFailsRule(new NoFragmentCycles, ' @@ -58,6 +85,9 @@ class NoFragmentCyclesTest extends TestCase ]); } + /** + * @it no spreading itself directly + */ public function testNoSpreadingItselfDirectly() { $this->expectFailsRule(new NoFragmentCycles, ' @@ -67,6 +97,9 @@ class NoFragmentCyclesTest extends TestCase ]); } + /** + * @it no spreading itself directly within inline fragment + */ public function testNoSpreadingItselfDirectlyWithinInlineFragment() { $this->expectFailsRule(new NoFragmentCycles, ' @@ -80,6 +113,9 @@ class NoFragmentCyclesTest extends TestCase ]); } + /** + * @it no spreading itself indirectly + */ public function testNoSpreadingItselfIndirectly() { $this->expectFailsRule(new NoFragmentCycles, ' @@ -93,6 +129,9 @@ class NoFragmentCyclesTest extends TestCase ]); } + /** + * @it no spreading itself indirectly reports opposite order + */ public function testNoSpreadingItselfIndirectlyReportsOppositeOrder() { $this->expectFailsRule(new NoFragmentCycles, ' @@ -106,6 +145,9 @@ class NoFragmentCyclesTest extends TestCase ]); } + /** + * @it no spreading itself indirectly within inline fragment + */ public function testNoSpreadingItselfIndirectlyWithinInlineFragment() { $this->expectFailsRule(new NoFragmentCycles, ' @@ -127,6 +169,9 @@ class NoFragmentCyclesTest extends TestCase ]); } + /** + * @it no spreading itself deeply + */ public function testNoSpreadingItselfDeeply() { $this->expectFailsRule(new NoFragmentCycles, ' @@ -136,30 +181,36 @@ class NoFragmentCyclesTest extends TestCase fragment fragX on Dog { ...fragY } fragment fragY on Dog { ...fragZ } fragment fragZ on Dog { ...fragO } - fragment fragO on Dog { ...fragA, ...fragX } + fragment fragO on Dog { ...fragP } + fragment fragP on Dog { ...fragA, ...fragX } ', [ FormattedError::create( - NoFragmentCycles::cycleErrorMessage('fragA', ['fragB', 'fragC', 'fragO']), + NoFragmentCycles::cycleErrorMessage('fragA', [ 'fragB', 'fragC', 'fragO', 'fragP' ]), [ new SourceLocation(2, 31), new SourceLocation(3, 31), new SourceLocation(4, 31), new SourceLocation(8, 31), + new SourceLocation(9, 31), ] ), FormattedError::create( - NoFragmentCycles::cycleErrorMessage('fragX', ['fragY', 'fragZ', 'fragO']), + NoFragmentCycles::cycleErrorMessage('fragO', [ 'fragP', 'fragX', 'fragY', 'fragZ' ]), [ + new SourceLocation(8, 31), + new SourceLocation(9, 41), new SourceLocation(5, 31), new SourceLocation(6, 31), new SourceLocation(7, 31), - new SourceLocation(8, 41), ] ) ]); } - public function testNoSpreadingItselfDeeplyTwoPathsNewRule() + /** + * @it no spreading itself deeply two paths + */ + public function testNoSpreadingItselfDeeplyTwoPaths() { $this->expectFailsRule(new NoFragmentCycles, ' fragment fragA on Dog { ...fragB, ...fragC } @@ -177,6 +228,56 @@ class NoFragmentCyclesTest extends TestCase ]); } + /** + * @it no spreading itself deeply two paths -- alt traverse order + */ + public function testNoSpreadingItselfDeeplyTwoPathsTraverseOrder() + { + $this->expectFailsRule(new NoFragmentCycles, ' + fragment fragA on Dog { ...fragC } + fragment fragB on Dog { ...fragC } + fragment fragC on Dog { ...fragA, ...fragB } + ', [ + FormattedError::create( + NoFragmentCycles::cycleErrorMessage('fragA', [ 'fragC' ]), + [new SourceLocation(2,31), new SourceLocation(4,31)] + ), + FormattedError::create( + NoFragmentCycles::cycleErrorMessage('fragC', [ 'fragB' ]), + [new SourceLocation(4, 41), new SourceLocation(3, 31)] + ) + ]); + } + + /** + * @it no spreading itself deeply and immediately + */ + public function testNoSpreadingItselfDeeplyAndImmediately() + { + $this->expectFailsRule(new NoFragmentCycles, ' + fragment fragA on Dog { ...fragB } + fragment fragB on Dog { ...fragB, ...fragC } + fragment fragC on Dog { ...fragA, ...fragB } + ', [ + FormattedError::create( + NoFragmentCycles::cycleErrorMessage('fragB', []), + [new SourceLocation(3, 31)] + ), + FormattedError::create( + NoFragmentCycles::cycleErrorMessage('fragA', [ 'fragB', 'fragC' ]), + [ + new SourceLocation(2, 31), + new SourceLocation(3, 41), + new SourceLocation(4, 31) + ] + ), + FormattedError::create( + NoFragmentCycles::cycleErrorMessage('fragB', [ 'fragC' ]), + [new SourceLocation(3, 41), new SourceLocation(4, 41)] + ) + ]); + } + private function cycleError($fargment, $spreadNames, $line, $column) { return FormattedError::create( diff --git a/tests/Validator/NoUndefinedVariablesTest.php b/tests/Validator/NoUndefinedVariablesTest.php index a91b70e..6bc86a3 100644 --- a/tests/Validator/NoUndefinedVariablesTest.php +++ b/tests/Validator/NoUndefinedVariablesTest.php @@ -9,6 +9,9 @@ class NoUndefinedVariablesTest extends TestCase { // Validate: No undefined variables + /** + * @it all variables defined + */ public function testAllVariablesDefined() { $this->expectPassesRule(new NoUndefinedVariables(), ' @@ -18,6 +21,9 @@ class NoUndefinedVariablesTest extends TestCase '); } + /** + * @it all variables deeply defined + */ public function testAllVariablesDeeplyDefined() { $this->expectPassesRule(new NoUndefinedVariables, ' @@ -31,6 +37,9 @@ class NoUndefinedVariablesTest extends TestCase '); } + /** + * @it all variables deeply in inline fragments defined + */ public function testAllVariablesDeeplyInInlineFragmentsDefined() { $this->expectPassesRule(new NoUndefinedVariables, ' @@ -48,6 +57,9 @@ class NoUndefinedVariablesTest extends TestCase '); } + /** + * @it all variables in fragments deeply defined + */ public function testAllVariablesInFragmentsDeeplyDefined() { $this->expectPassesRule(new NoUndefinedVariables, ' @@ -70,6 +82,9 @@ class NoUndefinedVariablesTest extends TestCase '); } + /** + * @it variable within single fragment defined in multiple operations + */ public function testVariableWithinSingleFragmentDefinedInMultipleOperations() { // variable within single fragment defined in multiple operations @@ -86,6 +101,9 @@ class NoUndefinedVariablesTest extends TestCase '); } + /** + * @it variable within fragments defined in operations + */ public function testVariableWithinFragmentsDefinedInOperations() { $this->expectPassesRule(new NoUndefinedVariables, ' @@ -104,6 +122,9 @@ class NoUndefinedVariablesTest extends TestCase '); } + /** + * @it variable within recursive fragment defined + */ public function testVariableWithinRecursiveFragmentDefined() { $this->expectPassesRule(new NoUndefinedVariables, ' @@ -118,6 +139,9 @@ class NoUndefinedVariablesTest extends TestCase '); } + /** + * @it variable not defined + */ public function testVariableNotDefined() { $this->expectFailsRule(new NoUndefinedVariables, ' @@ -125,10 +149,13 @@ class NoUndefinedVariablesTest extends TestCase field(a: $a, b: $b, c: $c, d: $d) } ', [ - $this->undefVar('d', 3, 39) + $this->undefVar('d', 3, 39, 'Foo', 2, 7) ]); } + /** + * @it variable not defined by un-named query + */ public function testVariableNotDefinedByUnNamedQuery() { $this->expectFailsRule(new NoUndefinedVariables, ' @@ -136,10 +163,13 @@ class NoUndefinedVariablesTest extends TestCase field(a: $a) } ', [ - $this->undefVar('a', 3, 18) + $this->undefVar('a', 3, 18, '', 2, 7) ]); } + /** + * @it multiple variables not defined + */ public function testMultipleVariablesNotDefined() { $this->expectFailsRule(new NoUndefinedVariables, ' @@ -147,11 +177,14 @@ class NoUndefinedVariablesTest extends TestCase field(a: $a, b: $b, c: $c) } ', [ - $this->undefVar('a', 3, 18), - $this->undefVar('c', 3, 32) + $this->undefVar('a', 3, 18, 'Foo', 2, 7), + $this->undefVar('c', 3, 32, 'Foo', 2, 7) ]); } + /** + * @it variable in fragment not defined by un-named query + */ public function testVariableInFragmentNotDefinedByUnNamedQuery() { $this->expectFailsRule(new NoUndefinedVariables, ' @@ -162,10 +195,13 @@ class NoUndefinedVariablesTest extends TestCase field(a: $a) } ', [ - $this->undefVar('a', 6, 18) + $this->undefVar('a', 6, 18, '', 2, 7) ]); } + /** + * @it variable in fragment not defined by operation + */ public function testVariableInFragmentNotDefinedByOperation() { $this->expectFailsRule(new NoUndefinedVariables, ' @@ -186,10 +222,13 @@ class NoUndefinedVariablesTest extends TestCase field(c: $c) } ', [ - $this->undefVarByOp('c', 16, 18, 'Foo', 2, 7) + $this->undefVar('c', 16, 18, 'Foo', 2, 7) ]); } + /** + * @it multiple variables in fragments not defined + */ public function testMultipleVariablesInFragmentsNotDefined() { $this->expectFailsRule(new NoUndefinedVariables, ' @@ -210,11 +249,14 @@ class NoUndefinedVariablesTest extends TestCase field(c: $c) } ', [ - $this->undefVarByOp('a', 6, 18, 'Foo', 2, 7), - $this->undefVarByOp('c', 16, 18, 'Foo', 2, 7) + $this->undefVar('a', 6, 18, 'Foo', 2, 7), + $this->undefVar('c', 16, 18, 'Foo', 2, 7) ]); } + /** + * @it single variable in fragment not defined by multiple operations + */ public function testSingleVariableInFragmentNotDefinedByMultipleOperations() { $this->expectFailsRule(new NoUndefinedVariables, ' @@ -228,11 +270,14 @@ class NoUndefinedVariablesTest extends TestCase field(a: $a, b: $b) } ', [ - $this->undefVarByOp('b', 9, 25, 'Foo', 2, 7), - $this->undefVarByOp('b', 9, 25, 'Bar', 5, 7) + $this->undefVar('b', 9, 25, 'Foo', 2, 7), + $this->undefVar('b', 9, 25, 'Bar', 5, 7) ]); } + /** + * @it variables in fragment not defined by multiple operations + */ public function testVariablesInFragmentNotDefinedByMultipleOperations() { $this->expectFailsRule(new NoUndefinedVariables, ' @@ -246,11 +291,14 @@ class NoUndefinedVariablesTest extends TestCase field(a: $a, b: $b) } ', [ - $this->undefVarByOp('a', 9, 18, 'Foo', 2, 7), - $this->undefVarByOp('b', 9, 25, 'Bar', 5, 7) + $this->undefVar('a', 9, 18, 'Foo', 2, 7), + $this->undefVar('b', 9, 25, 'Bar', 5, 7) ]); } + /** + * @it variable in fragment used by other operation + */ public function testVariableInFragmentUsedByOtherOperation() { $this->expectFailsRule(new NoUndefinedVariables, ' @@ -267,11 +315,14 @@ class NoUndefinedVariablesTest extends TestCase field(b: $b) } ', [ - $this->undefVarByOp('a', 9, 18, 'Foo', 2, 7), - $this->undefVarByOp('b', 12, 18, 'Bar', 5, 7) + $this->undefVar('a', 9, 18, 'Foo', 2, 7), + $this->undefVar('b', 12, 18, 'Bar', 5, 7) ]); } + /** + * @it multiple undefined variables produce multiple errors + */ public function testMultipleUndefinedVariablesProduceMultipleErrors() { $this->expectFailsRule(new NoUndefinedVariables, ' @@ -290,29 +341,27 @@ class NoUndefinedVariablesTest extends TestCase field2(c: $c) } ', [ - $this->undefVarByOp('a', 9, 19, 'Foo', 2, 7), - $this->undefVarByOp('c', 14, 19, 'Foo', 2, 7), - $this->undefVarByOp('a', 11, 19, 'Foo', 2, 7), - $this->undefVarByOp('b', 9, 26, 'Bar', 5, 7), - $this->undefVarByOp('c', 14, 19, 'Bar', 5, 7), - $this->undefVarByOp('b', 11, 26, 'Bar', 5, 7), + $this->undefVar('a', 9, 19, 'Foo', 2, 7), + $this->undefVar('a', 11, 19, 'Foo', 2, 7), + $this->undefVar('c', 14, 19, 'Foo', 2, 7), + $this->undefVar('b', 9, 26, 'Bar', 5, 7), + $this->undefVar('b', 11, 26, 'Bar', 5, 7), + $this->undefVar('c', 14, 19, 'Bar', 5, 7), ]); } - private function undefVar($varName, $line, $column) + private function undefVar($varName, $line, $column, $opName = null, $l2 = null, $c2 = null) { - return FormattedError::create( - NoUndefinedVariables::undefinedVarMessage($varName), - [new SourceLocation($line, $column)] - ); - } + $locs = [new SourceLocation($line, $column)]; + + if ($l2 && $c2) { + $locs[] = new SourceLocation($l2, $c2); + } - private function undefVarByOp($varName, $l1, $c1, $opName, $l2, $c2) - { return FormattedError::create( - NoUndefinedVariables::undefinedVarByOpMessage($varName, $opName), - [new SourceLocation($l1, $c1), new SourceLocation($l2, $c2)] + NoUndefinedVariables::undefinedVarMessage($varName, $opName), + $locs ); } } diff --git a/tests/Validator/NoUnusedFragmentsTest.php b/tests/Validator/NoUnusedFragmentsTest.php index 15b2152..53d5373 100644 --- a/tests/Validator/NoUnusedFragmentsTest.php +++ b/tests/Validator/NoUnusedFragmentsTest.php @@ -8,6 +8,10 @@ use GraphQL\Validator\Rules\NoUnusedFragments; class NoUnusedFragmentsTest extends TestCase { // Validate: No unused fragments + + /** + * @it all fragment names are used + */ public function testAllFragmentNamesAreUsed() { $this->expectPassesRule(new NoUnusedFragments(), ' @@ -32,6 +36,9 @@ class NoUnusedFragmentsTest extends TestCase '); } + /** + * @it all fragment names are used by multiple operations + */ public function testAllFragmentNamesAreUsedByMultipleOperations() { $this->expectPassesRule(new NoUnusedFragments, ' @@ -58,6 +65,9 @@ class NoUnusedFragmentsTest extends TestCase '); } + /** + * @it contains unknown fragments + */ public function testContainsUnknownFragments() { $this->expectFailsRule(new NoUnusedFragments, ' @@ -93,6 +103,9 @@ class NoUnusedFragmentsTest extends TestCase ]); } + /** + * @it contains unknown fragments with ref cycle + */ public function testContainsUnknownFragmentsWithRefCycle() { $this->expectFailsRule(new NoUnusedFragments, ' @@ -130,6 +143,9 @@ class NoUnusedFragmentsTest extends TestCase ]); } + /** + * @it contains unknown and undef fragments + */ public function testContainsUnknownAndUndefFragments() { diff --git a/tests/Validator/TestCase.php b/tests/Validator/TestCase.php index c2a6ed9..0df6386 100644 --- a/tests/Validator/TestCase.php +++ b/tests/Validator/TestCase.php @@ -3,6 +3,7 @@ namespace GraphQL\Tests\Validator; use GraphQL\Language\Parser; use GraphQL\Schema; +use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; @@ -43,12 +44,24 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase ], ]); + $Canine = new InterfaceType([ + 'name' => 'Canine', + 'fields' => function() { + return [ + 'name' => [ + 'type' => Type::string(), + 'args' => ['surname' => ['type' => Type::boolean()]] + ] + ]; + } + ]); + $DogCommand = new EnumType([ 'name' => 'DogCommand', 'values' => [ 'SIT' => ['value' => 0], 'HEEL' => ['value' => 1], - 'DOWN' => ['value' => 3] + 'DOWN' => ['value' => 2] ] ]); @@ -76,7 +89,7 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase 'args' => ['x' => ['type' => Type::int()], 'y' => ['type' => Type::int()]] ] ], - 'interfaces' => [$Being, $Pet] + 'interfaces' => [$Being, $Pet, $Canine] ]); $Cat = new ObjectType([ @@ -277,7 +290,15 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase ] ]); - $defaultSchema = new Schema($queryRoot); + $defaultSchema = new Schema([ + 'query' => $queryRoot, + 'directives' => [ + new Directive([ + 'name' => 'operationOnly', + 'locations' => [ 'QUERY' ], + ]) + ] + ]); return $defaultSchema; }