Updating validator rules for april2016 spec

This commit is contained in:
vladar 2016-04-25 03:57:09 +06:00
parent 17081cec1c
commit f1ddc98390
43 changed files with 1880 additions and 505 deletions

View File

@ -2,7 +2,7 @@
namespace GraphQL\Language\AST; namespace GraphQL\Language\AST;
class FragmentDefinition extends Node implements Definition class FragmentDefinition extends Node implements Definition, HasSelectionSet
{ {
public $kind = Node::FRAGMENT_DEFINITION; public $kind = Node::FRAGMENT_DEFINITION;

View File

@ -0,0 +1,10 @@
<?php
namespace GraphQL\Language\AST;
interface HasSelectionSet
{
/**
* export type Definition = OperationDefinition
* | FragmentDefinition
*/
}

View File

@ -1,7 +1,7 @@
<?php <?php
namespace GraphQL\Language\AST; namespace GraphQL\Language\AST;
class OperationDefinition extends Node implements Definition class OperationDefinition extends Node implements Definition, HasSelectionSet
{ {
/** /**
* @var string * @var string

View File

@ -2,12 +2,10 @@
namespace GraphQL\Language; namespace GraphQL\Language;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Utils\TypeInfo;
class Visitor class Visitor
{ {
const BREAK_VISIT = '@@BREAK@@';
const CONTINUE_VISIT = '@@CONTINUE@@';
/** /**
* Break visitor * Break visitor
* *
@ -244,7 +242,7 @@ class Visitor
throw new \Exception('Invalid AST Node: ' . json_encode($node)); throw new \Exception('Invalid AST Node: ' . json_encode($node));
} }
$visitFn = self::getVisitFn($visitor, $isLeaving, $node->kind); $visitFn = self::getVisitFn($visitor, $node->kind, $isLeaving);
if ($visitFn) { if ($visitFn) {
$result = call_user_func($visitFn, $node, $key, $parent, $path, $ancestors); $result = call_user_func($visitFn, $node, $key, $parent, $path, $ancestors);
@ -308,13 +306,100 @@ class Visitor
return $newRoot; 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 $visitor
* @param $isLeaving
* @param $kind * @param $kind
* @param $isLeaving
* @return null * @return null
*/ */
public static function getVisitFn($visitor, $isLeaving, $kind) public static function getVisitFn($visitor, $kind, $isLeaving)
{ {
if (!$visitor) { if (!$visitor) {
return null; return null;

View File

@ -1,6 +1,7 @@
<?php <?php
namespace GraphQL; namespace GraphQL;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\FieldDefinition;
@ -16,37 +17,128 @@ use GraphQL\Type\Introspection;
class Schema class Schema
{ {
protected $querySchema; /**
* @var ObjectType
*/
protected $_queryType;
protected $mutationSchema; /**
* @var ObjectType
*/
protected $_mutationType;
protected $subscriptionSchema; /**
* @var ObjectType
protected $_typeMap; */
protected $_subscriptionType;
/**
* @var Directive[]
*/
protected $_directives; protected $_directives;
public function __construct(Type $querySchema = null, Type $mutationSchema = null, Type $subscriptionSchema = null) /**
{ * @var array<string, Type>
Utils::invariant($querySchema || $mutationSchema, "Either query or mutation type must be set"); */
$this->querySchema = $querySchema; protected $_typeMap;
$this->mutationSchema = $mutationSchema;
$this->subscriptionSchema = $subscriptionSchema;
InterfaceType::loadImplementationToInterfaces(); /**
* @var array<string, ObjectType[]>
*/
protected $_implementations;
/**
* @var array<string, array<string, boolean>>
*/
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. // 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 = []; $map = [];
foreach ([$this->getQueryType(), $this->getMutationType(), Introspection::_schema()] as $type) { foreach ($initialTypes as $type) {
$this->_extractTypes($type, $map); $this->_extractTypes($type, $map);
} }
$this->_typeMap = $map + Type::getInternalTypes(); $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 // Enforce correct interface implementations
foreach ($this->_typeMap as $typeName => $type) { foreach ($this->_typeMap as $typeName => $type) {
if ($type instanceof ObjectType) { if ($type instanceof ObjectType) {
foreach ($type->getInterfaces() as $iface) { foreach ($type->getInterfaces() as $iface) {
$this->assertObjectImplementsInterface($type, $iface); $this->_assertObjectImplementsInterface($type, $iface);
} }
} }
} }
@ -57,7 +149,7 @@ class Schema
* @param InterfaceType $iface * @param InterfaceType $iface
* @throws \Exception * @throws \Exception
*/ */
private function assertObjectImplementsInterface(ObjectType $object, InterfaceType $iface) protected function _assertObjectImplementsInterface(ObjectType $object, InterfaceType $iface)
{ {
$objectFieldMap = $object->getFields(); $objectFieldMap = $object->getFields();
$ifaceFieldMap = $iface->getFields(); $ifaceFieldMap = $iface->getFields();
@ -73,7 +165,7 @@ class Schema
$objectField = $objectFieldMap[$fieldName]; $objectField = $objectFieldMap[$fieldName];
Utils::invariant( Utils::invariant(
$this->isEqualType($ifaceField->getType(), $objectField->getType()), $this->_isEqualType($ifaceField->getType(), $objectField->getType()),
"$iface.$fieldName expects type \"{$ifaceField->getType()}\" but " . "$iface.$fieldName expects type \"{$ifaceField->getType()}\" but " .
"$object.$fieldName provides type \"{$objectField->getType()}" "$object.$fieldName provides type \"{$objectField->getType()}"
); );
@ -93,7 +185,7 @@ class Schema
// Assert interface field arg type matches object field arg type. // Assert interface field arg type matches object field arg type.
// (invariant) // (invariant)
Utils::invariant( Utils::invariant(
$this->isEqualType($ifaceArg->getType(), $objectArg->getType()), $this->_isEqualType($ifaceArg->getType(), $objectArg->getType()),
"$iface.$fieldName($argName:) expects type \"{$ifaceArg->getType()}\" " . "$iface.$fieldName($argName:) expects type \"{$ifaceArg->getType()}\" " .
"but $object.$fieldName($argName:) provides " . "but $object.$fieldName($argName:) provides " .
"type \"{$objectArg->getType()}\"" "type \"{$objectArg->getType()}\""
@ -118,35 +210,105 @@ class Schema
* @param $typeB * @param $typeB
* @return bool * @return bool
*/ */
private function isEqualType($typeA, $typeB) protected function _isEqualType($typeA, $typeB)
{ {
if ($typeA instanceof NonNull && $typeB instanceof NonNull) { 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) { 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 $typeA === $typeB;
} }
/**
* @return ObjectType
*/
public function getQueryType() public function getQueryType()
{ {
return $this->querySchema; return $this->_queryType;
} }
/**
* @return ObjectType
*/
public function getMutationType() public function getMutationType()
{ {
return $this->mutationSchema; return $this->_mutationType;
} }
/**
* @return ObjectType
*/
public function getSubscriptionType() 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 * @param $name
* @return null * @return Directive
*/ */
public function getDirective($name) public function getDirective($name)
{ {
@ -158,26 +320,7 @@ class Schema
return null; return null;
} }
/** protected function _extractTypes($type, &$map)
* @return array<Directive>
*/
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)
{ {
if (!$type) { if (!$type) {
return $map; return $map;
@ -198,8 +341,8 @@ class Schema
$nestedTypes = []; $nestedTypes = [];
if ($type instanceof InterfaceType || $type instanceof UnionType) { if ($type instanceof UnionType) {
$nestedTypes = $type->getPossibleTypes(); $nestedTypes = $type->getTypes();
} }
if ($type instanceof ObjectType) { if ($type instanceof ObjectType) {
$nestedTypes = array_merge($nestedTypes, $type->getInterfaces()); $nestedTypes = array_merge($nestedTypes, $type->getInterfaces());
@ -218,10 +361,4 @@ class Schema
} }
return $map; return $map;
} }
public function getType($name)
{
$map = $this->getTypeMap();
return isset($map[$name]) ? $map[$name] : null;
}
} }

View File

@ -12,16 +12,16 @@ GraphQLUnionType;
/** /**
* @return array<ObjectType> * @return array<ObjectType>
*/ */
public function getPossibleTypes(); // public function getPossibleTypes();
/** /**
* @return ObjectType * @return ObjectType
*/ */
public function getObjectType($value, ResolveInfo $info); // public function getObjectType($value, ResolveInfo $info);
/** /**
* @param Type $type * @param Type $type
* @return bool * @return bool
*/ */
public function isPossibleType(Type $type); // public function isPossibleType(Type $type);
} }

View File

@ -5,6 +5,16 @@ class Directive
{ {
public static $internalDirectives; 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 * @return Directive
*/ */
@ -30,6 +40,11 @@ class Directive
'include' => new self([ 'include' => new self([
'name' => 'include', 'name' => 'include',
'description' => 'Directs the executor to include this field or fragment only when the `if` argument is true.', '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' => [ 'args' => [
new FieldArgument([ new FieldArgument([
'name' => 'if', 'name' => 'if',
@ -37,23 +52,22 @@ class Directive
'description' => 'Included when true.' 'description' => 'Included when true.'
]) ])
], ],
'onOperation' => false,
'onFragment' => true,
'onField' => true
]), ]),
'skip' => new self([ 'skip' => new self([
'name' => 'skip', 'name' => 'skip',
'description' => 'Directs the executor to skip this field or fragment when the `if` argument is true.', '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' => [ 'args' => [
new FieldArgument([ new FieldArgument([
'name' => 'if', 'name' => 'if',
'type' => Type::nonNull(Type::boolean()), 'type' => Type::nonNull(Type::boolean()),
'description' => 'Skipped when true' 'description' => 'Skipped when true'
]) ])
], ]
'onOperation' => false,
'onFragment' => true,
'onField' => true
]) ])
]; ];
} }
@ -70,26 +84,18 @@ class Directive
*/ */
public $description; public $description;
/**
* Values from self::$locationMap
*
* @var array
*/
public $locations;
/** /**
* @var FieldArgument[] * @var FieldArgument[]
*/ */
public $args; public $args;
/**
* @var boolean
*/
public $onOperation;
/**
* @var boolean
*/
public $onFragment;
/**
* @var boolean
*/
public $onField;
public function __construct(array $config) public function __construct(array $config)
{ {
foreach ($config as $key => $value) { foreach ($config as $key => $value) {

View File

@ -6,7 +6,7 @@ use GraphQL\Utils;
class UnionType extends Type implements AbstractType, OutputType, CompositeType class UnionType extends Type implements AbstractType, OutputType, CompositeType
{ {
/** /**
* @var Array<GraphQLObjectType> * @var ObjectType[]
*/ */
private $_types; private $_types;
@ -48,10 +48,16 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
$this->_config = $config; $this->_config = $config;
} }
/**
* @return array<ObjectType>
*/
public function getPossibleTypes() 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) { if ($this->_types instanceof \Closure) {
$this->_types = call_user_func($this->_types); $this->_types = call_user_func($this->_types);
@ -71,7 +77,7 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
if (null === $this->_possibleTypeNames) { if (null === $this->_possibleTypeNames) {
$this->_possibleTypeNames = []; $this->_possibleTypeNames = [];
foreach ($this->getPossibleTypes() as $possibleType) { foreach ($this->getTypes() as $possibleType) {
$this->_possibleTypeNames[$possibleType->name] = true; $this->_possibleTypeNames[$possibleType->name] = true;
} }
} }

View File

@ -41,7 +41,7 @@ class TypeInfo
return $innerType ? new NonNull($innerType) : null; 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); return $schema->getType($inputTypeAst->name->value);
} }
@ -197,11 +197,7 @@ class TypeInfo
// isCompositeType is a type refining predicate, so this is safe. // isCompositeType is a type refining predicate, so this is safe.
$compositeType = $namedType; $compositeType = $namedType;
} }
array_push($this->_parentTypeStack, $compositeType); $this->_parentTypeStack[] = $compositeType; // push
break;
case Node::DIRECTIVE:
$this->_directive = $schema->getDirective($node->name->value);
break; break;
case Node::FIELD: case Node::FIELD:
@ -210,8 +206,12 @@ class TypeInfo
if ($parentType) { if ($parentType) {
$fieldDef = self::_getFieldDef($schema, $parentType, $node); $fieldDef = self::_getFieldDef($schema, $parentType, $node);
} }
array_push($this->_fieldDefStack, $fieldDef); $this->_fieldDefStack[] = $fieldDef; // push
array_push($this->_typeStack, $fieldDef ? $fieldDef->getType() : null); $this->_typeStack[] = $fieldDef ? $fieldDef->getType() : null; // push
break;
case Node::DIRECTIVE:
$this->_directive = $schema->getDirective($node->name->value);
break; break;
case Node::OPERATION_DEFINITION: case Node::OPERATION_DEFINITION:
@ -220,18 +220,22 @@ class TypeInfo
$type = $schema->getQueryType(); $type = $schema->getQueryType();
} else if ($node->operation === 'mutation') { } else if ($node->operation === 'mutation') {
$type = $schema->getMutationType(); $type = $schema->getMutationType();
} else if ($node->operation === 'subscription') {
$type = $schema->getSubscriptionType();
} }
array_push($this->_typeStack, $type); $this->_typeStack[] = $type; // push
break; break;
case Node::INLINE_FRAGMENT: case Node::INLINE_FRAGMENT:
case Node::FRAGMENT_DEFINITION: case Node::FRAGMENT_DEFINITION:
$type = self::typeFromAST($schema, $node->typeCondition); $typeConditionAST = $node->typeCondition;
array_push($this->_typeStack, $type); $outputType = $typeConditionAST ? self::typeFromAST($schema, $typeConditionAST) : $this->getType();
$this->_typeStack[] = $outputType; // push
break; break;
case Node::VARIABLE_DEFINITION: case Node::VARIABLE_DEFINITION:
array_push($this->_inputTypeStack, self::typeFromAST($schema, $node->type)); $inputType = self::typeFromAST($schema, $node->type);
$this->_inputTypeStack[] = $inputType; // push
break; break;
case Node::ARGUMENT: case Node::ARGUMENT:
@ -244,15 +248,12 @@ class TypeInfo
} }
} }
$this->_argument = $argDef; $this->_argument = $argDef;
array_push($this->_inputTypeStack, $argType); $this->_inputTypeStack[] = $argType; // push
break; break;
case Node::LST: case Node::LST:
$listType = Type::getNullableType($this->getInputType()); $listType = Type::getNullableType($this->getInputType());
array_push( $this->_inputTypeStack[] = ($listType instanceof ListOfType ? $listType->getWrappedType() : null); // push
$this->_inputTypeStack,
$listType instanceof ListOfType ? $listType->getWrappedType() : null
);
break; break;
case Node::OBJECT_FIELD: case Node::OBJECT_FIELD:
@ -263,7 +264,7 @@ class TypeInfo
$inputField = isset($tmp[$node->name->value]) ? $tmp[$node->name->value] : null; $inputField = isset($tmp[$node->name->value]) ? $tmp[$node->name->value] : null;
$fieldType = $inputField ? $inputField->getType() : null; $fieldType = $inputField ? $inputField->getType() : null;
} }
array_push($this->_inputTypeStack, $fieldType); $this->_inputTypeStack[] = $fieldType;
break; break;
} }
} }

View File

@ -8,11 +8,13 @@ use GraphQL\Language\AST\FragmentSpread;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\Value; use GraphQL\Language\AST\Value;
use GraphQL\Language\AST\Variable; use GraphQL\Language\AST\Variable;
use GraphQL\Language\Printer;
use GraphQL\Language\Visitor; use GraphQL\Language\Visitor;
use GraphQL\Language\VisitorOperation; use GraphQL\Language\VisitorOperation;
use GraphQL\Schema; use GraphQL\Schema;
use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\ScalarType;
@ -27,6 +29,7 @@ use GraphQL\Validator\Rules\KnownArgumentNames;
use GraphQL\Validator\Rules\KnownDirectives; use GraphQL\Validator\Rules\KnownDirectives;
use GraphQL\Validator\Rules\KnownFragmentNames; use GraphQL\Validator\Rules\KnownFragmentNames;
use GraphQL\Validator\Rules\KnownTypeNames; use GraphQL\Validator\Rules\KnownTypeNames;
use GraphQL\Validator\Rules\LoneAnonymousOperation;
use GraphQL\Validator\Rules\NoFragmentCycles; use GraphQL\Validator\Rules\NoFragmentCycles;
use GraphQL\Validator\Rules\NoUndefinedVariables; use GraphQL\Validator\Rules\NoUndefinedVariables;
use GraphQL\Validator\Rules\NoUnusedFragments; use GraphQL\Validator\Rules\NoUnusedFragments;
@ -62,28 +65,31 @@ class DocumentValidator
{ {
if (null === self::$defaultRules) { if (null === self::$defaultRules) {
self::$defaultRules = [ self::$defaultRules = [
// new UniqueOperationNames, // 'UniqueOperationNames' => new UniqueOperationNames(),
// new LoneAnonymousOperation, 'LoneAnonymousOperation' => new LoneAnonymousOperation(),
'KnownTypeNames' => new KnownTypeNames(), 'KnownTypeNames' => new KnownTypeNames(),
'FragmentsOnCompositeTypes' => new FragmentsOnCompositeTypes(), 'FragmentsOnCompositeTypes' => new FragmentsOnCompositeTypes(),
'VariablesAreInputTypes' => new VariablesAreInputTypes(), 'VariablesAreInputTypes' => new VariablesAreInputTypes(),
'ScalarLeafs' => new ScalarLeafs(), 'ScalarLeafs' => new ScalarLeafs(),
'FieldsOnCorrectType' => new FieldsOnCorrectType(), 'FieldsOnCorrectType' => new FieldsOnCorrectType(),
// new UniqueFragmentNames, // 'UniqueFragmentNames' => new UniqueFragmentNames(),
'KnownFragmentNames' => new KnownFragmentNames(), 'KnownFragmentNames' => new KnownFragmentNames(),
'NoUnusedFragments' => new NoUnusedFragments(), 'NoUnusedFragments' => new NoUnusedFragments(),
'PossibleFragmentSpreads' => new PossibleFragmentSpreads(), 'PossibleFragmentSpreads' => new PossibleFragmentSpreads(),
'NoFragmentCycles' => new NoFragmentCycles(), 'NoFragmentCycles' => new NoFragmentCycles(),
// 'UniqueVariableNames' => new UniqueVariableNames(),
'NoUndefinedVariables' => new NoUndefinedVariables(), 'NoUndefinedVariables' => new NoUndefinedVariables(),
'NoUnusedVariables' => new NoUnusedVariables(), 'NoUnusedVariables' => new NoUnusedVariables(),
'KnownDirectives' => new KnownDirectives(), 'KnownDirectives' => new KnownDirectives(),
'KnownArgumentNames' => new KnownArgumentNames(), 'KnownArgumentNames' => new KnownArgumentNames(),
// new UniqueArgumentNames, // 'UniqueArgumentNames' => new UniqueArgumentNames(),
'ArgumentsOfCorrectType' => new ArgumentsOfCorrectType(), 'ArgumentsOfCorrectType' => new ArgumentsOfCorrectType(),
'ProvidedNonNullArguments' => new ProvidedNonNullArguments(), 'ProvidedNonNullArguments' => new ProvidedNonNullArguments(),
'DefaultValuesOfCorrectType' => new DefaultValuesOfCorrectType(), 'DefaultValuesOfCorrectType' => new DefaultValuesOfCorrectType(),
'VariablesInAllowedPosition' => new VariablesInAllowedPosition(), 'VariablesInAllowedPosition' => new VariablesInAllowedPosition(),
'OverlappingFieldsCanBeMerged' => new OverlappingFieldsCanBeMerged(), 'OverlappingFieldsCanBeMerged' => new OverlappingFieldsCanBeMerged(),
// 'UniqueInputFieldNames' => new UniqueInputFieldNames(),
// Query Security // Query Security
'QueryDepth' => new QueryDepth(QueryDepth::DISABLED), // default disabled 'QueryDepth' => new QueryDepth(QueryDepth::DISABLED), // default disabled
'QueryComplexity' => new QueryComplexity(QueryComplexity::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) 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; return $errors;
} }
@ -128,76 +135,109 @@ class DocumentValidator
return $arr; 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. // A value must be provided if the type is non-null.
if (!$valueAST) { if ($type instanceof NonNull) {
return !($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 (!$valueAST) {
if ($type instanceof NonNull) { return [];
return static::isValidLiteralValue($valueAST, $type->getWrappedType());
} }
// This function only tests literals, and assumes variables will provide // This function only tests literals, and assumes variables will provide
// values of the correct type. // values of the correct type.
if ($valueAST instanceof Variable) { if ($valueAST instanceof Variable) {
return true; return [];
}
if (!$valueAST instanceof Value) {
return false;
} }
// Lists accept a non-list value as a list of one. // Lists accept a non-list value as a list of one.
if ($type instanceof ListOfType) { if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType(); $itemType = $type->getWrappedType();
if ($valueAST instanceof ListValue) { if ($valueAST instanceof ListValue) {
foreach($valueAST->values as $itemAST) { $errors = [];
if (!static::isValidLiteralValue($itemAST, $itemType)) { foreach($valueAST->values as $index => $itemAST) {
return false; $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 { } else {
return static::isValidLiteralValue($valueAST, $itemType); return static::isValidLiteralValue($itemType, $valueAST);
} }
} }
// Scalar/Enum input checks to ensure the type can serialize the value to // Input objects check each defined field and look for undefined fields.
// 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.
if ($type instanceof InputObjectType) { if ($type instanceof InputObjectType) {
$fields = $type->getFields();
if ($valueAST->kind !== Node::OBJECT) { 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) { $fields = $type->getFields();
$fieldName = $field->name ?: $fieldKey; $errors = [];
if (!isset($fieldASTMap[$fieldName]) && $field->getType() instanceof NonNull) {
// Required fields missing // Ensure every provided field is defined.
return false; $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())) { // Ensure every defined field is valid.
return false; $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. Utils::invariant(
return false; $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. * while maintaining the visitor skip and break API.
* *
* @param Schema $schema * @param Schema $schema
* @param TypeInfo $typeInfo
* @param Document $documentAST * @param Document $documentAST
* @param array $rules * @param array $rules
* @return array * @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); $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 = []; $errors = [];
// TODO: convert to class // TODO: convert to class
@ -237,7 +286,7 @@ class DocumentValidator
if ($node->kind === Node::FRAGMENT_DEFINITION && $key !== null && !empty($instances[$i]['visitSpreadFragments'])) { if ($node->kind === Node::FRAGMENT_DEFINITION && $key !== null && !empty($instances[$i]['visitSpreadFragments'])) {
$result = Visitor::skipNode(); $result = Visitor::skipNode();
} else { } else {
$enter = Visitor::getVisitFn($instances[$i], false, $node->kind); $enter = Visitor::getVisitFn($instances[$i], $node->kind, false);
if ($enter instanceof \Closure) { if ($enter instanceof \Closure) {
// $enter = $enter->bindTo($instances[$i]); // $enter = $enter->bindTo($instances[$i]);
$result = call_user_func_array($enter, func_get_args()); $result = call_user_func_array($enter, func_get_args());
@ -266,7 +315,7 @@ class DocumentValidator
} else if ($result && static::isError($result)) { } else if ($result && static::isError($result)) {
static::append($errors, $result); static::append($errors, $result);
for ($j = $i - 1; $j >= 0; $j--) { 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) { if ($leaveFn) {
// $leaveFn = $leaveFn->bindTo($instances[$j]) // $leaveFn = $leaveFn->bindTo($instances[$j])
$result = call_user_func_array($leaveFn, func_get_args()); $result = call_user_func_array($leaveFn, func_get_args());
@ -316,7 +365,7 @@ class DocumentValidator
} }
continue; continue;
} }
$leaveFn = Visitor::getVisitFn($instances[$i], true, $node->kind); $leaveFn = Visitor::getVisitFn($instances[$i], $node->kind, true);
if ($leaveFn) { if ($leaveFn) {
// $leaveFn = $leaveFn.bindTo($instances[$i]); // $leaveFn = $leaveFn.bindTo($instances[$i]);

View File

@ -1,6 +1,8 @@
<?php <?php
namespace GraphQL\Validator; namespace GraphQL\Validator;
use GraphQL\Utils;
class Messages class Messages
{ {
static function missingArgMessage($fieldName, $argName, $typeName) static function missingArgMessage($fieldName, $argName, $typeName)
@ -13,23 +15,6 @@ class Messages
return "Argument $argName expected type $typeName but got: $value."; return "Argument $argName expected type $typeName but got: $value.";
} }
static function defaultForNonNullArgMessage($varName, $typeName, $guessTypeName)
{
return "Variable \$$varName of type $typeName " .
"is required and will never use the default value. " .
"Perhaps you meant to use type $guessTypeName.";
}
static function badValueForDefaultArgMessage($varName, $typeName, $value)
{
return "Variable \$$varName of type $typeName has invalid default value: $value.";
}
static function undefinedFieldMessage($field, $type)
{
return 'Cannot query field ' . $field . ' on ' . $type;
}
static function fragmentOnNonCompositeErrorMessage($fragName, $typeName) static function fragmentOnNonCompositeErrorMessage($fragName, $typeName)
{ {
return "Fragment $fragName cannot condition on non composite type \"$typeName\"."; return "Fragment $fragName cannot condition on non composite type \"$typeName\".";

View File

@ -16,9 +16,10 @@ use GraphQL\Validator\ValidationContext;
class ArgumentsOfCorrectType class ArgumentsOfCorrectType
{ {
static function badValueMessage($argName, $type, $value) static function badValueMessage($argName, $type, $value, $verboseErrors = [])
{ {
return "Argument \"$argName\" expected type \"$type\" but got: $value."; $message = $verboseErrors ? ("\n" . implode("\n", $verboseErrors)) : '';
return "Argument \"$argName\" has invalid value $value.$message";
} }
public function __invoke(ValidationContext $context) public function __invoke(ValidationContext $context)
@ -26,12 +27,17 @@ class ArgumentsOfCorrectType
return [ return [
Node::ARGUMENT => function(Argument $argAST) use ($context) { Node::ARGUMENT => function(Argument $argAST) use ($context) {
$argDef = $context->getArgument(); $argDef = $context->getArgument();
if ($argDef && !DocumentValidator::isValidLiteralValue($argAST->value, $argDef->getType())) { if ($argDef) {
return new Error( $errors = DocumentValidator::isValidLiteralValue($argDef->getType(), $argAST->value);
self::badValueMessage($argAST->name->value, $argDef->getType(), Printer::doPrint($argAST->value)),
[$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();
} }
]; ];
} }

View File

@ -13,6 +13,19 @@ use GraphQL\Validator\ValidationContext;
class DefaultValuesOfCorrectType 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) public function __invoke(ValidationContext $context)
{ {
return [ return [
@ -22,16 +35,19 @@ class DefaultValuesOfCorrectType
$type = $context->getInputType(); $type = $context->getInputType();
if ($type instanceof NonNull && $defaultValue) { if ($type instanceof NonNull && $defaultValue) {
return new Error( $context->reportError(new Error(
Messages::defaultForNonNullArgMessage($name, $type, $type->getWrappedType()), static::defaultForNonNullArgMessage($name, $type, $type->getWrappedType()),
[$defaultValue] [$defaultValue]
); ));
} }
if ($type && $defaultValue && !DocumentValidator::isValidLiteralValue($defaultValue, $type)) { if ($type && $defaultValue) {
return new Error( $errors = DocumentValidator::isValidLiteralValue($type, $defaultValue);
Messages::badValueForDefaultArgMessage($name, $type, Printer::doPrint($defaultValue)), if (!empty($errors)) {
[$defaultValue] $context->reportError(new Error(
); static::badValueForDefaultArgMessage($name, $type, Printer::doPrint($defaultValue), $errors),
[$defaultValue]
));
}
} }
return null; return null;
} }

View File

@ -5,11 +5,34 @@ namespace GraphQL\Validator\Rules;
use GraphQL\Error; use GraphQL\Error;
use GraphQL\Language\AST\Field; use GraphQL\Language\AST\Field;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Schema;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Utils;
use GraphQL\Validator\Messages; use GraphQL\Validator\Messages;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
class FieldsOnCorrectType 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) public function __invoke(ValidationContext $context)
{ {
return [ return [
@ -18,13 +41,71 @@ class FieldsOnCorrectType
if ($type) { if ($type) {
$fieldDef = $context->getFieldDef(); $fieldDef = $context->getFieldDef();
if (!$fieldDef) { if (!$fieldDef) {
return new Error( // This isn't valid. Let's find suggestions, if any.
Messages::undefinedFieldMessage($node->name->value, $type->name), $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] [$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;
}
} }

View File

@ -30,21 +30,21 @@ class FragmentsOnCompositeTypes
Node::INLINE_FRAGMENT => function(InlineFragment $node) use ($context) { Node::INLINE_FRAGMENT => function(InlineFragment $node) use ($context) {
$type = $context->getType(); $type = $context->getType();
if ($type && !Type::isCompositeType($type)) { if ($node->typeCondition && $type && !Type::isCompositeType($type)) {
return new Error( $context->reportError(new Error(
self::inlineFragmentOnNonCompositeErrorMessage($type), static::inlineFragmentOnNonCompositeErrorMessage($type),
[$node->typeCondition] [$node->typeCondition]
); ));
} }
}, },
Node::FRAGMENT_DEFINITION => function(FragmentDefinition $node) use ($context) { Node::FRAGMENT_DEFINITION => function(FragmentDefinition $node) use ($context) {
$type = $context->getType(); $type = $context->getType();
if ($type && !Type::isCompositeType($type)) { if ($type && !Type::isCompositeType($type)) {
return new Error( $context->reportError(new Error(
self::fragmentOnNonCompositeErrorMessage($node->name->value, Printer::doPrint($node->typeCondition)), static::fragmentOnNonCompositeErrorMessage($node->name->value, Printer::doPrint($node->typeCondition)),
[$node->typeCondition] [$node->typeCondition]
); ));
} }
} }
]; ];

View File

@ -40,10 +40,10 @@ class KnownArgumentNames
if (!$fieldArgDef) { if (!$fieldArgDef) {
$parentType = $context->getParentType(); $parentType = $context->getParentType();
Utils::invariant($parentType); Utils::invariant($parentType);
return new Error( $context->reportError(new Error(
self::unknownArgMessage($node->name->value, $fieldDef->name, $parentType->name), self::unknownArgMessage($node->name->value, $fieldDef->name, $parentType->name),
[$node] [$node]
); ));
} }
} }
} else if ($argumentOf->kind === Node::DIRECTIVE) { } else if ($argumentOf->kind === Node::DIRECTIVE) {
@ -57,10 +57,10 @@ class KnownArgumentNames
} }
} }
if (!$directiveArgDef) { if (!$directiveArgDef) {
return new Error( $context->reportError(new Error(
self::unknownDirectiveArgMessage($node->name->value, $directive->name), self::unknownDirectiveArgMessage($node->name->value, $directive->name),
[$node] [$node]
); ));
} }
} }
} }

View File

@ -12,6 +12,7 @@ use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\OperationDefinition; use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Validator\Messages; use GraphQL\Validator\Messages;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use GraphQL\Type\Definition\Directive as DirectiveDef;
class KnownDirectives class KnownDirectives
{ {
@ -20,9 +21,9 @@ class KnownDirectives
return "Unknown directive \"$directiveName\"."; 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) public function __invoke(ValidationContext $context)
@ -38,39 +39,44 @@ class KnownDirectives
} }
if (!$directiveDef) { if (!$directiveDef) {
return new Error( $context->reportError(new Error(
self::unknownDirectiveMessage($node->name->value), self::unknownDirectiveMessage($node->name->value),
[$node] [$node]
); ));
return ;
} }
$appliedTo = $ancestors[count($ancestors) - 1]; $appliedTo = $ancestors[count($ancestors) - 1];
$candidateLocation = $this->getLocationForAppliedNode($appliedTo);
if ($appliedTo instanceof OperationDefinition && !$directiveDef->onOperation) { if (!$candidateLocation) {
return new Error( $context->reportError(new Error(
self::misplacedDirectiveMessage($node->name->value, 'operation'), self::misplacedDirectiveMessage($node->name->value, $node->type),
[$node] [$node]
); ));
} } else if (!in_array($candidateLocation, $directiveDef->locations)) {
if ($appliedTo instanceof Field && !$directiveDef->onField) { $context->reportError(new Error(
return new Error( self::misplacedDirectiveMessage($node->name->value, $candidateLocation),
self::misplacedDirectiveMessage($node->name->value, 'field'), [ $node ]
[$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]
);
} }
} }
]; ];
} }
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'];
}
}
} }

View File

@ -21,10 +21,10 @@ class KnownFragmentNames
$fragmentName = $node->name->value; $fragmentName = $node->name->value;
$fragment = $context->getFragment($fragmentName); $fragment = $context->getFragment($fragmentName);
if (!$fragment) { if (!$fragment) {
return new Error( $context->reportError(new Error(
self::unknownFragmentMessage($fragmentName), self::unknownFragmentMessage($fragmentName),
[$node->name] [$node->name]
); ));
} }
} }
]; ];

View File

@ -6,6 +6,7 @@ use GraphQL\Error;
use GraphQL\Language\AST\Name; use GraphQL\Language\AST\Name;
use GraphQL\Language\AST\NamedType; use GraphQL\Language\AST\NamedType;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Language\Visitor;
use GraphQL\Validator\Messages; use GraphQL\Validator\Messages;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
@ -18,14 +19,19 @@ class KnownTypeNames
public function __invoke(ValidationContext $context) public function __invoke(ValidationContext $context)
{ {
$skip = function() {return Visitor::skipNode();};
return [ 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) { Node::NAMED_TYPE => function(NamedType $node, $key) use ($context) {
if ($key === 'type' || $key === 'typeCondition') { $typeName = $node->name->value;
$typeName = $node->name->value; $type = $context->getSchema()->getType($typeName);
$type = $context->getSchema()->getType($typeName); if (!$type) {
if (!$type) { $context->reportError(new Error(self::unknownTypeMessage($typeName), [$node]));
return new Error(self::unknownTypeMessage($typeName), [$node]);
}
} }
} }
]; ];

View File

@ -0,0 +1,46 @@
<?php
namespace GraphQL\Validator\Rules;
use GraphQL\Error;
use GraphQL\Language\AST\Document;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Utils;
use GraphQL\Validator\ValidationContext;
/**
* Lone anonymous operation
*
* A GraphQL document is only valid if when it contains an anonymous operation
* (the query short-hand) that it contains only that one operation definition.
*/
class LoneAnonymousOperation extends \PHPUnit_Framework_TestCase
{
static function anonOperationNotAloneMessage()
{
return 'This anonymous operation must be the only defined operation.';
}
public function __invoke(ValidationContext $context)
{
$operationCount = 0;
return [
Node::DOCUMENT => 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])
);
}
}
];
}
}

View File

@ -14,6 +14,7 @@ use GraphQL\Language\AST\FragmentDefinition;
use GraphQL\Language\AST\FragmentSpread; use GraphQL\Language\AST\FragmentSpread;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Language\Visitor; use GraphQL\Language\Visitor;
use GraphQL\Utils;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
class NoFragmentCycles class NoFragmentCycles
@ -24,83 +25,86 @@ class NoFragmentCycles
return "Cannot spread fragment \"$fragName\" within itself$via."; return "Cannot spread fragment \"$fragName\" within itself$via.";
} }
public $visitedFrags;
public $spreadPath;
public $spreadPathIndexByName;
public function __invoke(ValidationContext $context) public function __invoke(ValidationContext $context)
{ {
// Gather all the fragment spreads ASTs for each fragment definition. // Tracks already visited fragments to maintain O(N) and to ensure that cycles
// Importantly this does not include inline fragments. // are not redundantly reported.
$definitions = $context->getDocument()->definitions; $this->visitedFrags = [];
$spreadsInFragment = [];
foreach ($definitions as $node) {
if ($node instanceof FragmentDefinition) {
$spreadsInFragment[$node->name->value] = $this->gatherSpreads($node);
}
}
// Tracks spreads known to lead to cycles to ensure that cycles are not // Array of AST nodes used to produce meaningful errors
// redundantly reported. $this->spreadPath = [];
$knownToLeadToCycle = new \SplObjectStorage();
// Position in the spread path
$this->spreadPathIndexByName = [];
return [ return [
Node::FRAGMENT_DEFINITION => function(FragmentDefinition $node) use ($spreadsInFragment, $knownToLeadToCycle) { Node::OPERATION_DEFINITION => function () {
$errors = []; return Visitor::skipNode();
$initialName = $node->name->value; },
Node::FRAGMENT_DEFINITION => function (FragmentDefinition $node) use ($context) {
// Array of AST nodes used to produce meaningful errors if (!isset($this->visitedFrags[$node->name->value])) {
$spreadPath = []; $this->detectCycleRecursive($node, $context);
$this->detectCycleRecursive($initialName, $spreadsInFragment, $knownToLeadToCycle, $initialName, $spreadPath, $errors);
if (!empty($errors)) {
return $errors;
} }
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) { $spreadNodes = $context->getFragmentSpreads($fragment);
$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;
}
foreach ($spreadPath as $spread) { if (empty($spreadNodes)) {
if ($spread === $spreadNode) { return;
continue 2;
}
}
$spreadPath[] = $spreadNode;
$this->detectCycleRecursive($spreadNode->name->value, $spreadsInFragment, $knownToLeadToCycle, $initialName, $spreadPath, $errors);
array_pop($spreadPath);
} }
}
$this->spreadPathIndexByName[$fragmentName] = count($this->spreadPath);
private function gatherSpreads($node) for ($i = 0; $i < count($spreadNodes); $i++) {
{ $spreadNode = $spreadNodes[$i];
$spreadNodes = []; $spreadName = $spreadNode->name->value;
Visitor::visit($node, [ $cycleIndex = isset($this->spreadPathIndexByName[$spreadName]) ? $this->spreadPathIndexByName[$spreadName] : null;
Node::FRAGMENT_SPREAD => function(FragmentSpread $spread) use (&$spreadNodes) {
$spreadNodes[] = $spread; 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;
} }
} }

View File

@ -23,62 +23,43 @@ use GraphQL\Validator\ValidationContext;
*/ */
class NoUndefinedVariables class NoUndefinedVariables
{ {
static function undefinedVarMessage($varName) static function undefinedVarMessage($varName, $opName = null)
{ {
return "Variable \"$$varName\" is not defined."; return $opName
} ? "Variable \"$$varName\" is not defined by operation \"$opName\"."
: "Variable \"$$varName\" is not defined.";
static function undefinedVarByOpMessage($varName, $opName)
{
return "Variable \"$$varName\" is not defined by operation \"$opName\".";
} }
public function __invoke(ValidationContext $context) public function __invoke(ValidationContext $context)
{ {
$operation = null; $variableNameDefined = [];
$visitedFragmentNames = [];
$definedVariableNames = [];
return [ return [
// Visit FragmentDefinition after visiting FragmentSpread Node::OPERATION_DEFINITION => [
'visitSpreadFragments' => true, '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) { foreach ($usages as $usage) {
$operation = $node; $node = $usage['node'];
$visitedFragmentNames = []; $varName = $node->name->value;
$definedVariableNames = [];
}, if (empty($variableNameDefined[$varName])) {
Node::VARIABLE_DEFINITION => function(VariableDefinition $def) use (&$definedVariableNames) { $context->reportError(new Error(
$definedVariableNames[$def->variable->name->value] = true; self::undefinedVarMessage(
}, $varName,
Node::VARIABLE => function(Variable $variable, $key, $parent, $path, $ancestors) use (&$definedVariableNames, &$visitedFragmentNames, &$operation) { $operation->name ? $operation->name->value : null
$varName = $variable->name->value; ),
if (empty($definedVariableNames[$varName])) { [ $node, $operation ]
$withinFragment = false; ));
foreach ($ancestors as $ancestor) {
if ($ancestor instanceof FragmentDefinition) {
$withinFragment = true;
break;
} }
} }
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) { Node::VARIABLE_DEFINITION => function(VariableDefinition $def) use (&$variableNameDefined) {
// Only visit fragments of a particular name once per operation $variableNameDefined[$def->variable->name->value] = true;
if (!empty($visitedFragmentNames[$spreadAST->name->value])) {
return Visitor::skipNode();
}
$visitedFragmentNames[$spreadAST->name->value] = true;
} }
]; ];
} }

View File

@ -6,6 +6,7 @@ use GraphQL\Error;
use GraphQL\Language\AST\FragmentDefinition; use GraphQL\Language\AST\FragmentDefinition;
use GraphQL\Language\AST\FragmentSpread; use GraphQL\Language\AST\FragmentSpread;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\Node;
use GraphQL\Language\Visitor;
use GraphQL\Validator\Messages; use GraphQL\Validator\Messages;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
@ -16,63 +17,45 @@ class NoUnusedFragments
return "Fragment \"$fragName\" is never used."; return "Fragment \"$fragName\" is never used.";
} }
public $operationDefs;
public $fragmentDefs;
public function __invoke(ValidationContext $context) public function __invoke(ValidationContext $context)
{ {
$fragmentDefs = []; $this->operationDefs = [];
$spreadsWithinOperation = []; $this->fragmentDefs = [];
$fragAdjacencies = new \stdClass();
$spreadNames = new \stdClass();
return [ return [
Node::OPERATION_DEFINITION => function() use (&$spreadNames, &$spreadsWithinOperation) { Node::OPERATION_DEFINITION => function($node) {
$spreadNames = new \stdClass(); $this->operationDefs[] = $node;
$spreadsWithinOperation[] = $spreadNames; return Visitor::skipNode();
}, },
Node::FRAGMENT_DEFINITION => function(FragmentDefinition $def) use (&$fragmentDefs, &$spreadNames, &$fragAdjacencies) { Node::FRAGMENT_DEFINITION => function(FragmentDefinition $def) {
$fragmentDefs[] = $def; $this->fragmentDefs[] = $def;
$spreadNames = new \stdClass(); return Visitor::skipNode();
$fragAdjacencies->{$def->name->value} = $spreadNames;
},
Node::FRAGMENT_SPREAD => function(FragmentSpread $spread) use (&$spreadNames) {
$spreadNames->{$spread->name->value} = true;
}, },
Node::DOCUMENT => [ Node::DOCUMENT => [
'leave' => function() use (&$fragAdjacencies, &$spreadsWithinOperation, &$fragmentDefs) { 'leave' => function() use ($context) {
$fragmentNameUsed = []; $fragmentNameUsed = [];
foreach ($spreadsWithinOperation as $spreads) { foreach ($this->operationDefs as $operation) {
$this->reduceSpreadFragments($spreads, $fragmentNameUsed, $fragAdjacencies); foreach ($context->getRecursivelyReferencedFragments($operation) as $fragment) {
} $fragmentNameUsed[$fragment->name->value] = true;
}
$errors = []; }
foreach ($fragmentDefs as $def) {
if (empty($fragmentNameUsed[$def->name->value])) { foreach ($this->fragmentDefs as $fragmentDef) {
$errors[] = new Error( $fragName = $fragmentDef->name->value;
self::unusedFragMessage($def->name->value), if (empty($fragmentNameUsed[$fragName])) {
[$def] $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
);
}
}
}
}
} }

View File

@ -54,18 +54,15 @@ class OverlappingFieldsCanBeMerged
$conflicts = $this->findConflicts($fieldMap, $context, $comparedSet); $conflicts = $this->findConflicts($fieldMap, $context, $comparedSet);
if (!empty($conflicts)) { foreach ($conflicts as $conflict) {
return array_map(function ($conflict) { $responseName = $conflict[0][0];
$responseName = $conflict[0][0]; $reason = $conflict[0][1];
$reason = $conflict[0][1]; $fields = $conflict[1];
$fields = $conflict[1];
return new Error(
self::fieldsConflictMessage($responseName, $reason),
$fields
);
}, $conflicts);
$context->reportError(new Error(
self::fieldsConflictMessage($responseName, $reason),
$fields
));
} }
} }
] ]

View File

@ -32,10 +32,10 @@ class PossibleFragmentSpreads
$fragType = Type::getNamedType($context->getType()); $fragType = Type::getNamedType($context->getType());
$parentType = $context->getParentType(); $parentType = $context->getParentType();
if ($fragType && $parentType && !$this->doTypesOverlap($fragType, $parentType)) { if ($fragType && $parentType && !$this->doTypesOverlap($fragType, $parentType)) {
return new Error( $context->reportError(new Error(
self::typeIncompatibleAnonSpreadMessage($parentType, $fragType), self::typeIncompatibleAnonSpreadMessage($parentType, $fragType),
[$node] [$node]
); ));
} }
}, },
Node::FRAGMENT_SPREAD => function(FragmentSpread $node) use ($context) { Node::FRAGMENT_SPREAD => function(FragmentSpread $node) use ($context) {
@ -44,10 +44,10 @@ class PossibleFragmentSpreads
$parentType = $context->getParentType(); $parentType = $context->getParentType();
if ($fragType && $parentType && !$this->doTypesOverlap($fragType, $parentType)) { if ($fragType && $parentType && !$this->doTypesOverlap($fragType, $parentType)) {
return new Error( $context->reportError(new Error(
self::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType), self::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType),
[$node] [$node]
); ));
} }
} }
]; ];

View File

@ -33,7 +33,6 @@ class ProvidedNonNullArguments
if (!$fieldDef) { if (!$fieldDef) {
return Visitor::skipNode(); return Visitor::skipNode();
} }
$errors = [];
$argASTs = $fieldAST->arguments ?: []; $argASTs = $fieldAST->arguments ?: [];
$argASTMap = []; $argASTMap = [];
@ -43,16 +42,12 @@ class ProvidedNonNullArguments
foreach ($fieldDef->args as $argDef) { foreach ($fieldDef->args as $argDef) {
$argAST = isset($argASTMap[$argDef->name]) ? $argASTMap[$argDef->name] : null; $argAST = isset($argASTMap[$argDef->name]) ? $argASTMap[$argDef->name] : null;
if (!$argAST && $argDef->getType() instanceof NonNull) { if (!$argAST && $argDef->getType() instanceof NonNull) {
$errors[] = new Error( $context->reportError(new Error(
self::missingFieldArgMessage($fieldAST->name->value, $argDef->name, $argDef->getType()), self::missingFieldArgMessage($fieldAST->name->value, $argDef->name, $argDef->getType()),
[$fieldAST] [$fieldAST]
); ));
} }
} }
if (!empty($errors)) {
return $errors;
}
} }
], ],
Node::DIRECTIVE => [ Node::DIRECTIVE => [
@ -61,7 +56,6 @@ class ProvidedNonNullArguments
if (!$directiveDef) { if (!$directiveDef) {
return Visitor::skipNode(); return Visitor::skipNode();
} }
$errors = [];
$argASTs = $directiveAST->arguments ?: []; $argASTs = $directiveAST->arguments ?: [];
$argASTMap = []; $argASTMap = [];
foreach ($argASTs as $argAST) { foreach ($argASTs as $argAST) {
@ -71,15 +65,12 @@ class ProvidedNonNullArguments
foreach ($directiveDef->args as $argDef) { foreach ($directiveDef->args as $argDef) {
$argAST = isset($argASTMap[$argDef->name]) ? $argASTMap[$argDef->name] : null; $argAST = isset($argASTMap[$argDef->name]) ? $argASTMap[$argDef->name] : null;
if (!$argAST && $argDef->getType() instanceof NonNull) { if (!$argAST && $argDef->getType() instanceof NonNull) {
$errors[] = new Error( $context->reportError(new Error(
self::missingDirectiveArgMessage($directiveAST->name->value, $argDef->name, $argDef->getType()), self::missingDirectiveArgMessage($directiveAST->name->value, $argDef->name, $argDef->getType()),
[$directiveAST] [$directiveAST]
); ));
} }
} }
if (!empty($errors)) {
return $errors;
}
} }
] ]
]; ];

View File

@ -29,16 +29,16 @@ class ScalarLeafs
if ($type) { if ($type) {
if (Type::isLeafType($type)) { if (Type::isLeafType($type)) {
if ($node->selectionSet) { if ($node->selectionSet) {
return new Error( $context->reportError(new Error(
self::noSubselectionAllowedMessage($node->name->value, $type), self::noSubselectionAllowedMessage($node->name->value, $type),
[$node->selectionSet] [$node->selectionSet]
); ));
} }
} else if (!$node->selectionSet) { } else if (!$node->selectionSet) {
return new Error( $context->reportError(new Error(
self::requiredSubselectionMessage($node->name->value, $type), self::requiredSubselectionMessage($node->name->value, $type),
[$node] [$node]
); ));
} }
} }
} }

View File

@ -27,10 +27,10 @@ class VariablesAreInputTypes
// If the variable type is not an input type, return an error. // If the variable type is not an input type, return an error.
if ($type && !Type::isInputType($type)) { if ($type && !Type::isInputType($type)) {
$variableName = $node->variable->name->value; $variableName = $node->variable->name->value;
return new Error( $context->reportError(new Error(
self::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)), self::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)),
[ $node->type ] [ $node->type ]
); ));
} }
} }
]; ];

View File

@ -47,10 +47,10 @@ class VariablesInAllowedPosition
if ($varType && $inputType && if ($varType && $inputType &&
!$this->varTypeAllowedForType($this->effectiveType($varType, $varDef), $inputType) !$this->varTypeAllowedForType($this->effectiveType($varType, $varDef), $inputType)
) { ) {
return new Error( $context->reportError(new Error(
Messages::badVarPosMessage($varName, $varType, $inputType), Messages::badVarPosMessage($varName, $varType, $inputType),
[$variableAST] [$variableAST]
); ));
} }
} }
]; ];

View File

@ -1,5 +1,13 @@
<?php <?php
namespace GraphQL\Validator; namespace GraphQL\Validator;
use GraphQL\Language\AST\FragmentSpread;
use GraphQL\Language\AST\HasSelectionSet;
use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Language\AST\Variable;
use GraphQL\Language\Visitor;
use \SplObjectStorage;
use GraphQL\Error;
use GraphQL\Schema; use GraphQL\Schema;
use GraphQL\Language\AST\Document; use GraphQL\Language\AST\Document;
use GraphQL\Language\AST\FragmentDefinition; use GraphQL\Language\AST\FragmentDefinition;
@ -33,16 +41,69 @@ class ValidationContext
*/ */
private $_typeInfo; private $_typeInfo;
/**
* @var Error[]
*/
private $_errors;
/** /**
* @var array<string, FragmentDefinition> * @var array<string, FragmentDefinition>
*/ */
private $_fragments; 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) function __construct(Schema $schema, Document $ast, TypeInfo $typeInfo)
{ {
$this->_schema = $schema; $this->_schema = $schema;
$this->_ast = $ast; $this->_ast = $ast;
$this->_typeInfo = $typeInfo; $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; 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 * Returns OutputType
* *

View File

@ -7,25 +7,22 @@ use GraphQL\Validator\Rules\ArgumentsOfCorrectType;
class ArgumentsOfCorrectTypeTest extends TestCase class ArgumentsOfCorrectTypeTest extends TestCase
{ {
function missingArg($fieldName, $argName, $typeName, $line, $column) function badValue($argName, $typeName, $value, $line, $column, $errors = null)
{ {
return FormattedError::create( $realErrors = !$errors ? ["Expected type \"$typeName\", found $value."] : $errors;
Messages::missingArgMessage($fieldName, $argName, $typeName),
[new SourceLocation($line, $column)]
);
}
function badValue($argName, $typeName, $value, $line, $column)
{
return FormattedError::create( return FormattedError::create(
ArgumentsOfCorrectType::badValueMessage($argName, $typeName, $value), ArgumentsOfCorrectType::badValueMessage($argName, $typeName, $value, $realErrors),
[new SourceLocation($line, $column)] [new SourceLocation($line, $column)]
); );
} }
// Validate: Argument values of correct type // Validate: Argument values of correct type
// Valid values: // Valid values
/**
* @it Good int value
*/
public function testGoodIntValue() public function testGoodIntValue()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType(), ' $this->expectPassesRule(new ArgumentsOfCorrectType(), '
@ -37,6 +34,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Good boolean value
*/
public function testGoodBooleanValue() public function testGoodBooleanValue()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType(), ' $this->expectPassesRule(new ArgumentsOfCorrectType(), '
@ -48,6 +48,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Good string value
*/
public function testGoodStringValue() public function testGoodStringValue()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType(), ' $this->expectPassesRule(new ArgumentsOfCorrectType(), '
@ -59,6 +62,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Good float value
*/
public function testGoodFloatValue() public function testGoodFloatValue()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType(), ' $this->expectPassesRule(new ArgumentsOfCorrectType(), '
@ -70,6 +76,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Int into Float
*/
public function testIntIntoFloat() public function testIntIntoFloat()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType(), ' $this->expectPassesRule(new ArgumentsOfCorrectType(), '
@ -81,6 +90,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Int into ID
*/
public function testIntIntoID() public function testIntIntoID()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType(), ' $this->expectPassesRule(new ArgumentsOfCorrectType(), '
@ -92,6 +104,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it String into ID
*/
public function testStringIntoID() public function testStringIntoID()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType(), ' $this->expectPassesRule(new ArgumentsOfCorrectType(), '
@ -103,6 +118,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Good enum value
*/
public function testGoodEnumValue() public function testGoodEnumValue()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType(), ' $this->expectPassesRule(new ArgumentsOfCorrectType(), '
@ -115,6 +133,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase
} }
// Invalid String values // Invalid String values
/**
* @it Int into String
*/
public function testIntIntoString() public function testIntIntoString()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -128,6 +150,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it Float into String
*/
public function testFloatIntoString() public function testFloatIntoString()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -141,6 +166,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it Boolean into String
*/
public function testBooleanIntoString() public function testBooleanIntoString()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -154,6 +182,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it Unquoted String into String
*/
public function testUnquotedStringIntoString() public function testUnquotedStringIntoString()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -168,6 +199,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase
} }
// Invalid Int values // Invalid Int values
/**
* @it String into Int
*/
public function testStringIntoInt() public function testStringIntoInt()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -181,6 +216,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it Big Int into Int
*/
public function testBigIntIntoInt() public function testBigIntIntoInt()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -194,6 +232,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it Unquoted String into Int
*/
public function testUnquotedStringIntoInt() public function testUnquotedStringIntoInt()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -207,6 +248,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it Simple Float into Int
*/
public function testSimpleFloatIntoInt() public function testSimpleFloatIntoInt()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -220,6 +264,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it Float into Int
*/
public function testFloatIntoInt() public function testFloatIntoInt()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -234,6 +281,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase
} }
// Invalid Float values // Invalid Float values
/**
* @it String into Float
*/
public function testStringIntoFloat() public function testStringIntoFloat()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -247,6 +298,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it Boolean into Float
*/
public function testBooleanIntoFloat() public function testBooleanIntoFloat()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -260,6 +314,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it Unquoted into Float
*/
public function testUnquotedIntoFloat() public function testUnquotedIntoFloat()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -274,6 +331,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase
} }
// Invalid Boolean value // Invalid Boolean value
/**
* @it Int into Boolean
*/
public function testIntIntoBoolean() public function testIntIntoBoolean()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -287,6 +348,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it Float into Boolean
*/
public function testFloatIntoBoolean() public function testFloatIntoBoolean()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -300,6 +364,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it String into Boolean
*/
public function testStringIntoBoolean() public function testStringIntoBoolean()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -313,6 +380,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it Unquoted into Boolean
*/
public function testUnquotedIntoBoolean() public function testUnquotedIntoBoolean()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -327,6 +397,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase
} }
// Invalid ID value // Invalid ID value
/**
* @it Float into ID
*/
public function testFloatIntoID() public function testFloatIntoID()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -340,6 +414,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it Boolean into ID
*/
public function testBooleanIntoID() public function testBooleanIntoID()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -353,6 +430,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it Unquoted into ID
*/
public function testUnquotedIntoID() public function testUnquotedIntoID()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -367,6 +447,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase
} }
// Invalid Enum value // Invalid Enum value
/**
* @it Int into Enum
*/
public function testIntIntoEnum() public function testIntIntoEnum()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -380,6 +464,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it Float into Enum
*/
public function testFloatIntoEnum() public function testFloatIntoEnum()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -393,6 +480,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it String into Enum
*/
public function testStringIntoEnum() public function testStringIntoEnum()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -406,6 +496,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it Boolean into Enum
*/
public function testBooleanIntoEnum() public function testBooleanIntoEnum()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -419,6 +512,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it Unknown Enum Value into Enum
*/
public function testUnknownEnumValueIntoEnum() public function testUnknownEnumValueIntoEnum()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -432,6 +528,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it Different case Enum Value into Enum
*/
public function testDifferentCaseEnumValueIntoEnum() public function testDifferentCaseEnumValueIntoEnum()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $this->expectFailsRule(new ArgumentsOfCorrectType(), '
@ -446,6 +545,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase
} }
// Valid List value // Valid List value
/**
* @it Good list value
*/
public function testGoodListValue() public function testGoodListValue()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType(), ' $this->expectPassesRule(new ArgumentsOfCorrectType(), '
@ -457,6 +560,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Empty list value
*/
public function testEmptyListValue() public function testEmptyListValue()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType(), ' $this->expectPassesRule(new ArgumentsOfCorrectType(), '
@ -468,6 +574,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Single value into List
*/
public function testSingleValueIntoList() public function testSingleValueIntoList()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType(), ' $this->expectPassesRule(new ArgumentsOfCorrectType(), '
@ -480,6 +589,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase
} }
// Invalid List value // Invalid List value
/**
* @it Incorrect item type
*/
public function testIncorrectItemtype() public function testIncorrectItemtype()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $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() public function testSingleValueOfIncorrectType()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType(), ' $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 // Valid non-nullable value
/**
* @it Arg on optional arg
*/
public function testArgOnOptionalArg() public function testArgOnOptionalArg()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType(), ' $this->expectPassesRule(new ArgumentsOfCorrectType(), '
@ -518,6 +640,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it No Arg on optional arg
*/
public function testNoArgOnOptionalArg() public function testNoArgOnOptionalArg()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType(), ' $this->expectPassesRule(new ArgumentsOfCorrectType(), '
@ -529,6 +654,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Multiple args
*/
public function testMultipleArgs() public function testMultipleArgs()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType(), ' $this->expectPassesRule(new ArgumentsOfCorrectType(), '
@ -540,6 +668,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Multiple args reverse order
*/
public function testMultipleArgsReverseOrder() public function testMultipleArgsReverseOrder()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType(), ' $this->expectPassesRule(new ArgumentsOfCorrectType(), '
@ -551,6 +682,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it No args on multiple optional
*/
public function testNoArgsOnMultipleOptional() public function testNoArgsOnMultipleOptional()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType(), ' $this->expectPassesRule(new ArgumentsOfCorrectType(), '
@ -562,6 +696,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it One arg on multiple optional
*/
public function testOneArgOnMultipleOptional() public function testOneArgOnMultipleOptional()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType, ' $this->expectPassesRule(new ArgumentsOfCorrectType, '
@ -573,6 +710,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Second arg on multiple optional
*/
public function testSecondArgOnMultipleOptional() public function testSecondArgOnMultipleOptional()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType, ' $this->expectPassesRule(new ArgumentsOfCorrectType, '
@ -584,6 +724,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Multiple reqs on mixedList
*/
public function testMultipleReqsOnMixedList() public function testMultipleReqsOnMixedList()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType, ' $this->expectPassesRule(new ArgumentsOfCorrectType, '
@ -595,6 +738,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Multiple reqs and one opt on mixedList
*/
public function testMultipleReqsAndOneOptOnMixedList() public function testMultipleReqsAndOneOptOnMixedList()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType, ' $this->expectPassesRule(new ArgumentsOfCorrectType, '
@ -606,6 +752,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it All reqs and opts on mixedList
*/
public function testAllReqsAndOptsOnMixedList() public function testAllReqsAndOptsOnMixedList()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType, ' $this->expectPassesRule(new ArgumentsOfCorrectType, '
@ -618,6 +767,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase
} }
// Invalid non-nullable value // Invalid non-nullable value
/**
* @it Incorrect value type
*/
public function testIncorrectValueType() public function testIncorrectValueType()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType, ' $this->expectFailsRule(new ArgumentsOfCorrectType, '
@ -627,11 +780,14 @@ class ArgumentsOfCorrectTypeTest extends TestCase
} }
} }
', [ ', [
$this->badValue('req2', 'Int!', '"two"', 4, 32), $this->badValue('req2', 'Int', '"two"', 4, 32),
$this->badValue('req1', 'Int!', '"one"', 4, 45), $this->badValue('req1', 'Int', '"one"', 4, 45),
]); ]);
} }
/**
* @it Incorrect value and missing argument
*/
public function testIncorrectValueAndMissingArgument() public function testIncorrectValueAndMissingArgument()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType, ' $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 // Valid input object value
/**
* @it Optional arg, despite required field in type
*/
public function testOptionalArgDespiteRequiredFieldInType() public function testOptionalArgDespiteRequiredFieldInType()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType, ' $this->expectPassesRule(new ArgumentsOfCorrectType, '
@ -658,6 +818,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Partial object, only required
*/
public function testPartialObjectOnlyRequired() public function testPartialObjectOnlyRequired()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType, ' $this->expectPassesRule(new ArgumentsOfCorrectType, '
@ -669,6 +832,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Partial object, required field can be falsey
*/
public function testPartialObjectRequiredFieldCanBeFalsey() public function testPartialObjectRequiredFieldCanBeFalsey()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType, ' $this->expectPassesRule(new ArgumentsOfCorrectType, '
@ -680,6 +846,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Partial object, including required
*/
public function testPartialObjectIncludingRequired() public function testPartialObjectIncludingRequired()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType, ' $this->expectPassesRule(new ArgumentsOfCorrectType, '
@ -691,6 +860,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Full object
*/
public function testFullObject() public function testFullObject()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType, ' $this->expectPassesRule(new ArgumentsOfCorrectType, '
@ -708,6 +880,9 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Full object with fields in different order
*/
public function testFullObjectWithFieldsInDifferentOrder() public function testFullObjectWithFieldsInDifferentOrder()
{ {
$this->expectPassesRule(new ArgumentsOfCorrectType(), ' $this->expectPassesRule(new ArgumentsOfCorrectType(), '
@ -726,6 +901,10 @@ class ArgumentsOfCorrectTypeTest extends TestCase
} }
// Invalid input object value // Invalid input object value
/**
* @it Partial object, missing required
*/
public function testPartialObjectMissingRequired() public function testPartialObjectMissingRequired()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType, ' $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() public function testPartialObjectInvalidFieldType()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType, ' $this->expectFailsRule(new ArgumentsOfCorrectType, '
@ -756,11 +940,15 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'ComplexInput', 'ComplexInput',
'{stringListField: ["one", 2], requiredField: true}', '{stringListField: ["one", 2], requiredField: true}',
4, 4,
41 41,
[ 'In field "stringListField": In element #1: Expected type "String", found 2.' ]
), ),
]); ]);
} }
/**
* @it Partial object, unknown field arg
*/
public function testPartialObjectUnknownFieldArg() public function testPartialObjectUnknownFieldArg()
{ {
$this->expectFailsRule(new ArgumentsOfCorrectType, ' $this->expectFailsRule(new ArgumentsOfCorrectType, '
@ -778,8 +966,45 @@ class ArgumentsOfCorrectTypeTest extends TestCase
'ComplexInput', 'ComplexInput',
'{requiredField: true, unknownField: "value"}', '{requiredField: true, unknownField: "value"}',
4, 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),
]);
}
} }

View File

@ -10,6 +10,9 @@ class DefaultValuesOfCorrectTypeTest extends TestCase
{ {
// Validate: Variable default values of correct type // Validate: Variable default values of correct type
/**
* @it variables with no default values
*/
public function testVariablesWithNoDefaultValues() public function testVariablesWithNoDefaultValues()
{ {
$this->expectPassesRule(new DefaultValuesOfCorrectType, ' $this->expectPassesRule(new DefaultValuesOfCorrectType, '
@ -19,6 +22,9 @@ class DefaultValuesOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it required variables without default values
*/
public function testRequiredVariablesWithoutDefaultValues() public function testRequiredVariablesWithoutDefaultValues()
{ {
$this->expectPassesRule(new DefaultValuesOfCorrectType, ' $this->expectPassesRule(new DefaultValuesOfCorrectType, '
@ -28,6 +34,9 @@ class DefaultValuesOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it variables with valid default values
*/
public function testVariablesWithValidDefaultValues() public function testVariablesWithValidDefaultValues()
{ {
$this->expectPassesRule(new DefaultValuesOfCorrectType, ' $this->expectPassesRule(new DefaultValuesOfCorrectType, '
@ -41,6 +50,9 @@ class DefaultValuesOfCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it no required variables with default values
*/
public function testNoRequiredVariablesWithDefaultValues() public function testNoRequiredVariablesWithDefaultValues()
{ {
$this->expectFailsRule(new DefaultValuesOfCorrectType, ' $this->expectFailsRule(new DefaultValuesOfCorrectType, '
@ -53,6 +65,9 @@ class DefaultValuesOfCorrectTypeTest extends TestCase
]); ]);
} }
/**
* @it variables with invalid default values
*/
public function testVariablesWithInvalidDefaultValues() public function testVariablesWithInvalidDefaultValues()
{ {
$this->expectFailsRule(new DefaultValuesOfCorrectType, ' $this->expectFailsRule(new DefaultValuesOfCorrectType, '
@ -64,12 +79,21 @@ class DefaultValuesOfCorrectTypeTest extends TestCase
dog { name } dog { name }
} }
', [ ', [
$this->badValue('a', 'Int', '"one"', 3, 19), $this->badValue('a', 'Int', '"one"', 3, 19, [
$this->badValue('b', 'String', '4', 4, 22), 'Expected type "Int", found "one".'
$this->badValue('c', 'ComplexInput', '"notverycomplex"', 5, 28) ]),
$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() public function testComplexVariablesMissingRequiredField()
{ {
$this->expectFailsRule(new DefaultValuesOfCorrectType, ' $this->expectFailsRule(new DefaultValuesOfCorrectType, '
@ -77,10 +101,15 @@ class DefaultValuesOfCorrectTypeTest extends TestCase
dog { name } 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() public function testListVariablesWithInvalidItem()
{ {
$this->expectFailsRule(new DefaultValuesOfCorrectType, ' $this->expectFailsRule(new DefaultValuesOfCorrectType, '
@ -88,22 +117,26 @@ class DefaultValuesOfCorrectTypeTest extends TestCase
dog { name } 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) private function defaultForNonNullArg($varName, $typeName, $guessTypeName, $line, $column)
{ {
return FormattedError::create( return FormattedError::create(
Messages::defaultForNonNullArgMessage($varName, $typeName, $guessTypeName), DefaultValuesOfCorrectType::defaultForNonNullArgMessage($varName, $typeName, $guessTypeName),
[ new SourceLocation($line, $column) ] [ 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( return FormattedError::create(
Messages::badValueForDefaultArgMessage($varName, $typeName, $val), DefaultValuesOfCorrectType::badValueForDefaultArgMessage($varName, $typeName, $val, $realErrors),
[ new SourceLocation($line, $column) ] [ new SourceLocation($line, $column) ]
); );
} }

View File

@ -9,6 +9,10 @@ use GraphQL\Validator\Rules\FieldsOnCorrectType;
class FieldsOnCorrectTypeTest extends TestCase class FieldsOnCorrectTypeTest extends TestCase
{ {
// Validate: Fields on correct type // Validate: Fields on correct type
/**
* @it Object field selection
*/
public function testObjectFieldSelection() public function testObjectFieldSelection()
{ {
$this->expectPassesRule(new FieldsOnCorrectType(), ' $this->expectPassesRule(new FieldsOnCorrectType(), '
@ -19,6 +23,9 @@ class FieldsOnCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Aliased object field selection
*/
public function testAliasedObjectFieldSelection() public function testAliasedObjectFieldSelection()
{ {
$this->expectPassesRule(new FieldsOnCorrectType, ' $this->expectPassesRule(new FieldsOnCorrectType, '
@ -29,6 +36,9 @@ class FieldsOnCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Interface field selection
*/
public function testInterfaceFieldSelection() public function testInterfaceFieldSelection()
{ {
$this->expectPassesRule(new FieldsOnCorrectType, ' $this->expectPassesRule(new FieldsOnCorrectType, '
@ -39,6 +49,9 @@ class FieldsOnCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Aliased interface field selection
*/
public function testAliasedInterfaceFieldSelection() public function testAliasedInterfaceFieldSelection()
{ {
$this->expectPassesRule(new FieldsOnCorrectType, ' $this->expectPassesRule(new FieldsOnCorrectType, '
@ -48,6 +61,9 @@ class FieldsOnCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Lying alias selection
*/
public function testLyingAliasSelection() public function testLyingAliasSelection()
{ {
$this->expectPassesRule(new FieldsOnCorrectType, ' $this->expectPassesRule(new FieldsOnCorrectType, '
@ -57,6 +73,9 @@ class FieldsOnCorrectTypeTest extends TestCase
'); ');
} }
/**
* @it Ignores fields on unknown type
*/
public function testIgnoresFieldsOnUnknownType() public function testIgnoresFieldsOnUnknownType()
{ {
$this->expectPassesRule(new FieldsOnCorrectType, ' $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() public function testFieldNotDefinedOnFragment()
{ {
$this->expectFailsRule(new FieldsOnCorrectType, ' $this->expectFailsRule(new FieldsOnCorrectType, '
fragment fieldNotDefined on Dog { fragment fieldNotDefined on Dog {
meowVolume 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, ' $this->expectFailsRule(new FieldsOnCorrectType, '
fragment deepFieldNotDefined on Dog { fragment deepFieldNotDefined on Dog {
@ -84,10 +127,13 @@ class FieldsOnCorrectTypeTest extends TestCase
deeper_unknown_field 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() public function testSubFieldNotDefined()
{ {
$this->expectFailsRule(new FieldsOnCorrectType, ' $this->expectFailsRule(new FieldsOnCorrectType, '
@ -96,10 +142,13 @@ class FieldsOnCorrectTypeTest extends TestCase
unknown_field 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() public function testFieldNotDefinedOnInlineFragment()
{ {
$this->expectFailsRule(new FieldsOnCorrectType, ' $this->expectFailsRule(new FieldsOnCorrectType, '
@ -108,50 +157,65 @@ class FieldsOnCorrectTypeTest extends TestCase
meowVolume meowVolume
} }
}', }',
[$this->undefinedField('meowVolume', 'Dog', 4, 11)] [$this->undefinedField('meowVolume', 'Dog', [], 4, 11)]
); );
} }
/**
* @it Aliased field target not defined
*/
public function testAliasedFieldTargetNotDefined() public function testAliasedFieldTargetNotDefined()
{ {
$this->expectFailsRule(new FieldsOnCorrectType, ' $this->expectFailsRule(new FieldsOnCorrectType, '
fragment aliasedFieldTargetNotDefined on Dog { fragment aliasedFieldTargetNotDefined on Dog {
volume : mooVolume volume : mooVolume
}', }',
[$this->undefinedField('mooVolume', 'Dog', 3, 9)] [$this->undefinedField('mooVolume', 'Dog', [], 3, 9)]
); );
} }
/**
* @it Aliased lying field target not defined
*/
public function testAliasedLyingFieldTargetNotDefined() public function testAliasedLyingFieldTargetNotDefined()
{ {
$this->expectFailsRule(new FieldsOnCorrectType, ' $this->expectFailsRule(new FieldsOnCorrectType, '
fragment aliasedLyingFieldTargetNotDefined on Dog { fragment aliasedLyingFieldTargetNotDefined on Dog {
barkVolume : kawVolume barkVolume : kawVolume
}', }',
[$this->undefinedField('kawVolume', 'Dog', 3, 9)] [$this->undefinedField('kawVolume', 'Dog', [], 3, 9)]
); );
} }
/**
* @it Not defined on interface
*/
public function testNotDefinedOnInterface() public function testNotDefinedOnInterface()
{ {
$this->expectFailsRule(new FieldsOnCorrectType, ' $this->expectFailsRule(new FieldsOnCorrectType, '
fragment notDefinedOnInterface on Pet { fragment notDefinedOnInterface on Pet {
tailLength tailLength
}', }',
[$this->undefinedField('tailLength', 'Pet', 3, 9)] [$this->undefinedField('tailLength', 'Pet', [], 3, 9)]
); );
} }
/**
* @it Defined on implementors but not on interface
*/
public function testDefinedOnImplmentorsButNotOnInterface() public function testDefinedOnImplmentorsButNotOnInterface()
{ {
$this->expectFailsRule(new FieldsOnCorrectType, ' $this->expectFailsRule(new FieldsOnCorrectType, '
fragment definedOnImplementorsButNotInterface on Pet { fragment definedOnImplementorsButNotInterface on Pet {
nickname nickname
}', }',
[$this->undefinedField('nickname', 'Pet', 3, 9)] [$this->undefinedField('nickname', 'Pet', [ 'Cat', 'Dog' ], 3, 9)]
); );
} }
/**
* @it Meta field selection on union
*/
public function testMetaFieldSelectionOnUnion() public function testMetaFieldSelectionOnUnion()
{ {
$this->expectPassesRule(new FieldsOnCorrectType, ' $this->expectPassesRule(new FieldsOnCorrectType, '
@ -161,26 +225,35 @@ class FieldsOnCorrectTypeTest extends TestCase
); );
} }
/**
* @it Direct field selection on union
*/
public function testDirectFieldSelectionOnUnion() public function testDirectFieldSelectionOnUnion()
{ {
$this->expectFailsRule(new FieldsOnCorrectType, ' $this->expectFailsRule(new FieldsOnCorrectType, '
fragment directFieldSelectionOnUnion on CatOrDog { fragment directFieldSelectionOnUnion on CatOrDog {
directField directField
}', }',
[$this->undefinedField('directField', 'CatOrDog', 3, 9)] [$this->undefinedField('directField', 'CatOrDog', [], 3, 9)]
); );
} }
/**
* @it Defined on implementors queried on union
*/
public function testDefinedOnImplementorsQueriedOnUnion() public function testDefinedOnImplementorsQueriedOnUnion()
{ {
$this->expectFailsRule(new FieldsOnCorrectType, ' $this->expectFailsRule(new FieldsOnCorrectType, '
fragment definedOnImplementorsQueriedOnUnion on CatOrDog { fragment definedOnImplementorsQueriedOnUnion on CatOrDog {
name 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() public function testValidFieldInInlineFragment()
{ {
$this->expectPassesRule(new FieldsOnCorrectType, ' $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( return FormattedError::create(
Messages::undefinedFieldMessage($field, $type), FieldsOnCorrectType::undefinedFieldMessage($field, $type, $suggestions),
[new SourceLocation($line, $column)] [new SourceLocation($line, $column)]
); );
} }

View File

@ -9,6 +9,9 @@ class FragmentsOnCompositeTypesTest extends TestCase
{ {
// Validate: Fragments on composite types // Validate: Fragments on composite types
/**
* @it object is valid fragment type
*/
public function testObjectIsValidFragmentType() public function testObjectIsValidFragmentType()
{ {
$this->expectPassesRule(new FragmentsOnCompositeTypes, ' $this->expectPassesRule(new FragmentsOnCompositeTypes, '
@ -18,6 +21,9 @@ class FragmentsOnCompositeTypesTest extends TestCase
'); ');
} }
/**
* @it interface is valid fragment type
*/
public function testInterfaceIsValidFragmentType() public function testInterfaceIsValidFragmentType()
{ {
$this->expectPassesRule(new FragmentsOnCompositeTypes, ' $this->expectPassesRule(new FragmentsOnCompositeTypes, '
@ -27,6 +33,9 @@ class FragmentsOnCompositeTypesTest extends TestCase
'); ');
} }
/**
* @it object is valid inline fragment type
*/
public function testObjectIsValidInlineFragmentType() public function testObjectIsValidInlineFragmentType()
{ {
$this->expectPassesRule(new FragmentsOnCompositeTypes, ' $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() public function testUnionIsValidFragmentType()
{ {
$this->expectPassesRule(new FragmentsOnCompositeTypes, ' $this->expectPassesRule(new FragmentsOnCompositeTypes, '
@ -47,6 +73,9 @@ class FragmentsOnCompositeTypesTest extends TestCase
'); ');
} }
/**
* @it scalar is invalid fragment type
*/
public function testScalarIsInvalidFragmentType() public function testScalarIsInvalidFragmentType()
{ {
$this->expectFailsRule(new FragmentsOnCompositeTypes, ' $this->expectFailsRule(new FragmentsOnCompositeTypes, '
@ -57,6 +86,9 @@ class FragmentsOnCompositeTypesTest extends TestCase
[$this->error('scalarFragment', 'Boolean', 2, 34)]); [$this->error('scalarFragment', 'Boolean', 2, 34)]);
} }
/**
* @it enum is invalid fragment type
*/
public function testEnumIsInvalidFragmentType() public function testEnumIsInvalidFragmentType()
{ {
$this->expectFailsRule(new FragmentsOnCompositeTypes, ' $this->expectFailsRule(new FragmentsOnCompositeTypes, '
@ -67,6 +99,9 @@ class FragmentsOnCompositeTypesTest extends TestCase
[$this->error('scalarFragment', 'FurColor', 2, 34)]); [$this->error('scalarFragment', 'FurColor', 2, 34)]);
} }
/**
* @it input object is invalid fragment type
*/
public function testInputObjectIsInvalidFragmentType() public function testInputObjectIsInvalidFragmentType()
{ {
$this->expectFailsRule(new FragmentsOnCompositeTypes, ' $this->expectFailsRule(new FragmentsOnCompositeTypes, '
@ -77,6 +112,9 @@ class FragmentsOnCompositeTypesTest extends TestCase
[$this->error('inputFragment', 'ComplexInput', 2, 33)]); [$this->error('inputFragment', 'ComplexInput', 2, 33)]);
} }
/**
* @it scalar is invalid inline fragment type
*/
public function testScalarIsInvalidInlineFragmentType() public function testScalarIsInvalidInlineFragmentType()
{ {
$this->expectFailsRule(new FragmentsOnCompositeTypes, ' $this->expectFailsRule(new FragmentsOnCompositeTypes, '

View File

@ -8,6 +8,10 @@ use GraphQL\Validator\Rules\KnownArgumentNames;
class KnownArgumentNamesTest extends TestCase class KnownArgumentNamesTest extends TestCase
{ {
// Validate: Known argument names: // Validate: Known argument names:
/**
* @it single arg is known
*/
public function testSingleArgIsKnown() public function testSingleArgIsKnown()
{ {
$this->expectPassesRule(new KnownArgumentNames, ' $this->expectPassesRule(new KnownArgumentNames, '
@ -17,6 +21,9 @@ class KnownArgumentNamesTest extends TestCase
'); ');
} }
/**
* @it multiple args are known
*/
public function testMultipleArgsAreKnown() public function testMultipleArgsAreKnown()
{ {
$this->expectPassesRule(new KnownArgumentNames, ' $this->expectPassesRule(new KnownArgumentNames, '
@ -26,6 +33,9 @@ class KnownArgumentNamesTest extends TestCase
'); ');
} }
/**
* @it ignores args of unknown fields
*/
public function testIgnoresArgsOfUnknownFields() public function testIgnoresArgsOfUnknownFields()
{ {
$this->expectPassesRule(new KnownArgumentNames, ' $this->expectPassesRule(new KnownArgumentNames, '
@ -35,6 +45,9 @@ class KnownArgumentNamesTest extends TestCase
'); ');
} }
/**
* @it multiple args in reverse order are known
*/
public function testMultipleArgsInReverseOrderAreKnown() public function testMultipleArgsInReverseOrderAreKnown()
{ {
$this->expectPassesRule(new KnownArgumentNames, ' $this->expectPassesRule(new KnownArgumentNames, '
@ -44,6 +57,9 @@ class KnownArgumentNamesTest extends TestCase
'); ');
} }
/**
* @it no args on optional arg
*/
public function testNoArgsOnOptionalArg() public function testNoArgsOnOptionalArg()
{ {
$this->expectPassesRule(new KnownArgumentNames, ' $this->expectPassesRule(new KnownArgumentNames, '
@ -53,6 +69,9 @@ class KnownArgumentNamesTest extends TestCase
'); ');
} }
/**
* @it args are known deeply
*/
public function testArgsAreKnownDeeply() public function testArgsAreKnownDeeply()
{ {
$this->expectPassesRule(new KnownArgumentNames, ' $this->expectPassesRule(new KnownArgumentNames, '
@ -71,6 +90,9 @@ class KnownArgumentNamesTest extends TestCase
'); ');
} }
/**
* @it directive args are known
*/
public function testDirectiveArgsAreKnown() public function testDirectiveArgsAreKnown()
{ {
$this->expectPassesRule(new KnownArgumentNames, ' $this->expectPassesRule(new KnownArgumentNames, '
@ -80,6 +102,9 @@ class KnownArgumentNamesTest extends TestCase
'); ');
} }
/**
* @it undirective args are invalid
*/
public function testUndirectiveArgsAreInvalid() public function testUndirectiveArgsAreInvalid()
{ {
$this->expectFailsRule(new KnownArgumentNames, ' $this->expectFailsRule(new KnownArgumentNames, '
@ -91,6 +116,9 @@ class KnownArgumentNamesTest extends TestCase
]); ]);
} }
/**
* @it invalid arg name
*/
public function testInvalidArgName() public function testInvalidArgName()
{ {
$this->expectFailsRule(new KnownArgumentNames, ' $this->expectFailsRule(new KnownArgumentNames, '
@ -102,6 +130,9 @@ class KnownArgumentNamesTest extends TestCase
]); ]);
} }
/**
* @it unknown args amongst known args
*/
public function testUnknownArgsAmongstKnownArgs() public function testUnknownArgsAmongstKnownArgs()
{ {
$this->expectFailsRule(new KnownArgumentNames, ' $this->expectFailsRule(new KnownArgumentNames, '
@ -114,6 +145,9 @@ class KnownArgumentNamesTest extends TestCase
]); ]);
} }
/**
* @it unknown args deeply
*/
public function testUnknownArgsDeeply() public function testUnknownArgsDeeply()
{ {
$this->expectFailsRule(new KnownArgumentNames, ' $this->expectFailsRule(new KnownArgumentNames, '

View File

@ -8,6 +8,10 @@ use GraphQL\Validator\Rules\KnownDirectives;
class KnownDirectivesTest extends TestCase class KnownDirectivesTest extends TestCase
{ {
// Validate: Known directives // Validate: Known directives
/**
* @it with no directives
*/
public function testWithNoDirectives() public function testWithNoDirectives()
{ {
$this->expectPassesRule(new KnownDirectives, ' $this->expectPassesRule(new KnownDirectives, '
@ -22,6 +26,9 @@ class KnownDirectivesTest extends TestCase
'); ');
} }
/**
* @it with known directives
*/
public function testWithKnownDirectives() public function testWithKnownDirectives()
{ {
$this->expectPassesRule(new KnownDirectives, ' $this->expectPassesRule(new KnownDirectives, '
@ -36,6 +43,9 @@ class KnownDirectivesTest extends TestCase
'); ');
} }
/**
* @it with unknown directive
*/
public function testWithUnknownDirective() public function testWithUnknownDirective()
{ {
$this->expectFailsRule(new KnownDirectives, ' $this->expectFailsRule(new KnownDirectives, '
@ -49,6 +59,9 @@ class KnownDirectivesTest extends TestCase
]); ]);
} }
/**
* @it with many unknown directives
*/
public function testWithManyUnknownDirectives() public function testWithManyUnknownDirectives()
{ {
$this->expectFailsRule(new KnownDirectives, ' $this->expectFailsRule(new KnownDirectives, '
@ -70,6 +83,9 @@ class KnownDirectivesTest extends TestCase
]); ]);
} }
/**
* @it with well placed directives
*/
public function testWithWellPlacedDirectives() public function testWithWellPlacedDirectives()
{ {
$this->expectPassesRule(new KnownDirectives, ' $this->expectPassesRule(new KnownDirectives, '
@ -82,15 +98,20 @@ class KnownDirectivesTest extends TestCase
'); ');
} }
/**
* @it with misplaced directives
*/
public function testWithMisplacedDirectives() public function testWithMisplacedDirectives()
{ {
$this->expectFailsRule(new KnownDirectives, ' $this->expectFailsRule(new KnownDirectives, '
query Foo @include(if: true) { query Foo @include(if: true) {
name name @operationOnly
...Frag ...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),
]); ]);
} }

View File

@ -9,6 +9,9 @@ class KnownFragmentNamesTest extends TestCase
{ {
// Validate: Known fragment names // Validate: Known fragment names
/**
* @it known fragment names are valid
*/
public function testKnownFragmentNamesAreValid() public function testKnownFragmentNamesAreValid()
{ {
$this->expectPassesRule(new KnownFragmentNames, ' $this->expectPassesRule(new KnownFragmentNames, '
@ -33,6 +36,9 @@ class KnownFragmentNamesTest extends TestCase
'); ');
} }
/**
* @it unknown fragment names are invalid
*/
public function testUnknownFragmentNamesAreInvalid() public function testUnknownFragmentNamesAreInvalid()
{ {
$this->expectFailsRule(new KnownFragmentNames, ' $this->expectFailsRule(new KnownFragmentNames, '

View File

@ -9,6 +9,9 @@ class KnownTypeNamesTest extends TestCase
{ {
// Validate: Known type names // Validate: Known type names
/**
* @it known type names are valid
*/
public function testKnownTypeNamesAreValid() public function testKnownTypeNamesAreValid()
{ {
$this->expectPassesRule(new KnownTypeNames, ' $this->expectPassesRule(new KnownTypeNames, '
@ -23,6 +26,9 @@ class KnownTypeNamesTest extends TestCase
'); ');
} }
/**
* @it unknown type names are invalid
*/
public function testUnknownTypeNamesAreInvalid() public function testUnknownTypeNamesAreInvalid()
{ {
$this->expectFailsRule(new KnownTypeNames, ' $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) private function unknownType($typeName, $line, $column)
{ {
return FormattedError::create( return FormattedError::create(

View File

@ -0,0 +1,127 @@
<?php
namespace GraphQL\Tests\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\SourceLocation;
use GraphQL\Validator\Rules\LoneAnonymousOperation;
class LoneAnonymousOperationTest extends TestCase
{
// Validate: Anonymous operation must be alone
/**
* @it no operations
*/
public function testNoOperations()
{
$this->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)]
);
}
}

View File

@ -9,6 +9,9 @@ class NoFragmentCyclesTest extends TestCase
{ {
// Validate: No circular fragment spreads // Validate: No circular fragment spreads
/**
* @it single reference is valid
*/
public function testSingleReferenceIsValid() public function testSingleReferenceIsValid()
{ {
$this->expectPassesRule(new NoFragmentCycles(), ' $this->expectPassesRule(new NoFragmentCycles(), '
@ -17,6 +20,9 @@ class NoFragmentCyclesTest extends TestCase
'); ');
} }
/**
* @it spreading twice is not circular
*/
public function testSpreadingTwiceIsNotCircular() public function testSpreadingTwiceIsNotCircular()
{ {
$this->expectPassesRule(new NoFragmentCycles, ' $this->expectPassesRule(new NoFragmentCycles, '
@ -25,6 +31,9 @@ class NoFragmentCyclesTest extends TestCase
'); ');
} }
/**
* @it spreading twice indirectly is not circular
*/
public function testSpreadingTwiceIndirectlyIsNotCircular() public function testSpreadingTwiceIndirectlyIsNotCircular()
{ {
$this->expectPassesRule(new NoFragmentCycles, ' $this->expectPassesRule(new NoFragmentCycles, '
@ -34,6 +43,9 @@ class NoFragmentCyclesTest extends TestCase
'); ');
} }
/**
* @it double spread within abstract types
*/
public function testDoubleSpreadWithinAbstractTypes() public function testDoubleSpreadWithinAbstractTypes()
{ {
$this->expectPassesRule(new NoFragmentCycles, ' $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() public function testSpreadingRecursivelyWithinFieldFails()
{ {
$this->expectFailsRule(new NoFragmentCycles, ' $this->expectFailsRule(new NoFragmentCycles, '
@ -58,6 +85,9 @@ class NoFragmentCyclesTest extends TestCase
]); ]);
} }
/**
* @it no spreading itself directly
*/
public function testNoSpreadingItselfDirectly() public function testNoSpreadingItselfDirectly()
{ {
$this->expectFailsRule(new NoFragmentCycles, ' $this->expectFailsRule(new NoFragmentCycles, '
@ -67,6 +97,9 @@ class NoFragmentCyclesTest extends TestCase
]); ]);
} }
/**
* @it no spreading itself directly within inline fragment
*/
public function testNoSpreadingItselfDirectlyWithinInlineFragment() public function testNoSpreadingItselfDirectlyWithinInlineFragment()
{ {
$this->expectFailsRule(new NoFragmentCycles, ' $this->expectFailsRule(new NoFragmentCycles, '
@ -80,6 +113,9 @@ class NoFragmentCyclesTest extends TestCase
]); ]);
} }
/**
* @it no spreading itself indirectly
*/
public function testNoSpreadingItselfIndirectly() public function testNoSpreadingItselfIndirectly()
{ {
$this->expectFailsRule(new NoFragmentCycles, ' $this->expectFailsRule(new NoFragmentCycles, '
@ -93,6 +129,9 @@ class NoFragmentCyclesTest extends TestCase
]); ]);
} }
/**
* @it no spreading itself indirectly reports opposite order
*/
public function testNoSpreadingItselfIndirectlyReportsOppositeOrder() public function testNoSpreadingItselfIndirectlyReportsOppositeOrder()
{ {
$this->expectFailsRule(new NoFragmentCycles, ' $this->expectFailsRule(new NoFragmentCycles, '
@ -106,6 +145,9 @@ class NoFragmentCyclesTest extends TestCase
]); ]);
} }
/**
* @it no spreading itself indirectly within inline fragment
*/
public function testNoSpreadingItselfIndirectlyWithinInlineFragment() public function testNoSpreadingItselfIndirectlyWithinInlineFragment()
{ {
$this->expectFailsRule(new NoFragmentCycles, ' $this->expectFailsRule(new NoFragmentCycles, '
@ -127,6 +169,9 @@ class NoFragmentCyclesTest extends TestCase
]); ]);
} }
/**
* @it no spreading itself deeply
*/
public function testNoSpreadingItselfDeeply() public function testNoSpreadingItselfDeeply()
{ {
$this->expectFailsRule(new NoFragmentCycles, ' $this->expectFailsRule(new NoFragmentCycles, '
@ -136,30 +181,36 @@ class NoFragmentCyclesTest extends TestCase
fragment fragX on Dog { ...fragY } fragment fragX on Dog { ...fragY }
fragment fragY on Dog { ...fragZ } fragment fragY on Dog { ...fragZ }
fragment fragZ on Dog { ...fragO } fragment fragZ on Dog { ...fragO }
fragment fragO on Dog { ...fragA, ...fragX } fragment fragO on Dog { ...fragP }
fragment fragP on Dog { ...fragA, ...fragX }
', [ ', [
FormattedError::create( FormattedError::create(
NoFragmentCycles::cycleErrorMessage('fragA', ['fragB', 'fragC', 'fragO']), NoFragmentCycles::cycleErrorMessage('fragA', [ 'fragB', 'fragC', 'fragO', 'fragP' ]),
[ [
new SourceLocation(2, 31), new SourceLocation(2, 31),
new SourceLocation(3, 31), new SourceLocation(3, 31),
new SourceLocation(4, 31), new SourceLocation(4, 31),
new SourceLocation(8, 31), new SourceLocation(8, 31),
new SourceLocation(9, 31),
] ]
), ),
FormattedError::create( 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(5, 31),
new SourceLocation(6, 31), new SourceLocation(6, 31),
new SourceLocation(7, 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, ' $this->expectFailsRule(new NoFragmentCycles, '
fragment fragA on Dog { ...fragB, ...fragC } 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) private function cycleError($fargment, $spreadNames, $line, $column)
{ {
return FormattedError::create( return FormattedError::create(

View File

@ -9,6 +9,9 @@ class NoUndefinedVariablesTest extends TestCase
{ {
// Validate: No undefined variables // Validate: No undefined variables
/**
* @it all variables defined
*/
public function testAllVariablesDefined() public function testAllVariablesDefined()
{ {
$this->expectPassesRule(new NoUndefinedVariables(), ' $this->expectPassesRule(new NoUndefinedVariables(), '
@ -18,6 +21,9 @@ class NoUndefinedVariablesTest extends TestCase
'); ');
} }
/**
* @it all variables deeply defined
*/
public function testAllVariablesDeeplyDefined() public function testAllVariablesDeeplyDefined()
{ {
$this->expectPassesRule(new NoUndefinedVariables, ' $this->expectPassesRule(new NoUndefinedVariables, '
@ -31,6 +37,9 @@ class NoUndefinedVariablesTest extends TestCase
'); ');
} }
/**
* @it all variables deeply in inline fragments defined
*/
public function testAllVariablesDeeplyInInlineFragmentsDefined() public function testAllVariablesDeeplyInInlineFragmentsDefined()
{ {
$this->expectPassesRule(new NoUndefinedVariables, ' $this->expectPassesRule(new NoUndefinedVariables, '
@ -48,6 +57,9 @@ class NoUndefinedVariablesTest extends TestCase
'); ');
} }
/**
* @it all variables in fragments deeply defined
*/
public function testAllVariablesInFragmentsDeeplyDefined() public function testAllVariablesInFragmentsDeeplyDefined()
{ {
$this->expectPassesRule(new NoUndefinedVariables, ' $this->expectPassesRule(new NoUndefinedVariables, '
@ -70,6 +82,9 @@ class NoUndefinedVariablesTest extends TestCase
'); ');
} }
/**
* @it variable within single fragment defined in multiple operations
*/
public function testVariableWithinSingleFragmentDefinedInMultipleOperations() public function testVariableWithinSingleFragmentDefinedInMultipleOperations()
{ {
// variable within single fragment defined in multiple operations // 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() public function testVariableWithinFragmentsDefinedInOperations()
{ {
$this->expectPassesRule(new NoUndefinedVariables, ' $this->expectPassesRule(new NoUndefinedVariables, '
@ -104,6 +122,9 @@ class NoUndefinedVariablesTest extends TestCase
'); ');
} }
/**
* @it variable within recursive fragment defined
*/
public function testVariableWithinRecursiveFragmentDefined() public function testVariableWithinRecursiveFragmentDefined()
{ {
$this->expectPassesRule(new NoUndefinedVariables, ' $this->expectPassesRule(new NoUndefinedVariables, '
@ -118,6 +139,9 @@ class NoUndefinedVariablesTest extends TestCase
'); ');
} }
/**
* @it variable not defined
*/
public function testVariableNotDefined() public function testVariableNotDefined()
{ {
$this->expectFailsRule(new NoUndefinedVariables, ' $this->expectFailsRule(new NoUndefinedVariables, '
@ -125,10 +149,13 @@ class NoUndefinedVariablesTest extends TestCase
field(a: $a, b: $b, c: $c, d: $d) 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() public function testVariableNotDefinedByUnNamedQuery()
{ {
$this->expectFailsRule(new NoUndefinedVariables, ' $this->expectFailsRule(new NoUndefinedVariables, '
@ -136,10 +163,13 @@ class NoUndefinedVariablesTest extends TestCase
field(a: $a) field(a: $a)
} }
', [ ', [
$this->undefVar('a', 3, 18) $this->undefVar('a', 3, 18, '', 2, 7)
]); ]);
} }
/**
* @it multiple variables not defined
*/
public function testMultipleVariablesNotDefined() public function testMultipleVariablesNotDefined()
{ {
$this->expectFailsRule(new NoUndefinedVariables, ' $this->expectFailsRule(new NoUndefinedVariables, '
@ -147,11 +177,14 @@ class NoUndefinedVariablesTest extends TestCase
field(a: $a, b: $b, c: $c) field(a: $a, b: $b, c: $c)
} }
', [ ', [
$this->undefVar('a', 3, 18), $this->undefVar('a', 3, 18, 'Foo', 2, 7),
$this->undefVar('c', 3, 32) $this->undefVar('c', 3, 32, 'Foo', 2, 7)
]); ]);
} }
/**
* @it variable in fragment not defined by un-named query
*/
public function testVariableInFragmentNotDefinedByUnNamedQuery() public function testVariableInFragmentNotDefinedByUnNamedQuery()
{ {
$this->expectFailsRule(new NoUndefinedVariables, ' $this->expectFailsRule(new NoUndefinedVariables, '
@ -162,10 +195,13 @@ class NoUndefinedVariablesTest extends TestCase
field(a: $a) 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() public function testVariableInFragmentNotDefinedByOperation()
{ {
$this->expectFailsRule(new NoUndefinedVariables, ' $this->expectFailsRule(new NoUndefinedVariables, '
@ -186,10 +222,13 @@ class NoUndefinedVariablesTest extends TestCase
field(c: $c) 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() public function testMultipleVariablesInFragmentsNotDefined()
{ {
$this->expectFailsRule(new NoUndefinedVariables, ' $this->expectFailsRule(new NoUndefinedVariables, '
@ -210,11 +249,14 @@ class NoUndefinedVariablesTest extends TestCase
field(c: $c) field(c: $c)
} }
', [ ', [
$this->undefVarByOp('a', 6, 18, 'Foo', 2, 7), $this->undefVar('a', 6, 18, 'Foo', 2, 7),
$this->undefVarByOp('c', 16, 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() public function testSingleVariableInFragmentNotDefinedByMultipleOperations()
{ {
$this->expectFailsRule(new NoUndefinedVariables, ' $this->expectFailsRule(new NoUndefinedVariables, '
@ -228,11 +270,14 @@ class NoUndefinedVariablesTest extends TestCase
field(a: $a, b: $b) field(a: $a, b: $b)
} }
', [ ', [
$this->undefVarByOp('b', 9, 25, 'Foo', 2, 7), $this->undefVar('b', 9, 25, 'Foo', 2, 7),
$this->undefVarByOp('b', 9, 25, 'Bar', 5, 7) $this->undefVar('b', 9, 25, 'Bar', 5, 7)
]); ]);
} }
/**
* @it variables in fragment not defined by multiple operations
*/
public function testVariablesInFragmentNotDefinedByMultipleOperations() public function testVariablesInFragmentNotDefinedByMultipleOperations()
{ {
$this->expectFailsRule(new NoUndefinedVariables, ' $this->expectFailsRule(new NoUndefinedVariables, '
@ -246,11 +291,14 @@ class NoUndefinedVariablesTest extends TestCase
field(a: $a, b: $b) field(a: $a, b: $b)
} }
', [ ', [
$this->undefVarByOp('a', 9, 18, 'Foo', 2, 7), $this->undefVar('a', 9, 18, 'Foo', 2, 7),
$this->undefVarByOp('b', 9, 25, 'Bar', 5, 7) $this->undefVar('b', 9, 25, 'Bar', 5, 7)
]); ]);
} }
/**
* @it variable in fragment used by other operation
*/
public function testVariableInFragmentUsedByOtherOperation() public function testVariableInFragmentUsedByOtherOperation()
{ {
$this->expectFailsRule(new NoUndefinedVariables, ' $this->expectFailsRule(new NoUndefinedVariables, '
@ -267,11 +315,14 @@ class NoUndefinedVariablesTest extends TestCase
field(b: $b) field(b: $b)
} }
', [ ', [
$this->undefVarByOp('a', 9, 18, 'Foo', 2, 7), $this->undefVar('a', 9, 18, 'Foo', 2, 7),
$this->undefVarByOp('b', 12, 18, 'Bar', 5, 7) $this->undefVar('b', 12, 18, 'Bar', 5, 7)
]); ]);
} }
/**
* @it multiple undefined variables produce multiple errors
*/
public function testMultipleUndefinedVariablesProduceMultipleErrors() public function testMultipleUndefinedVariablesProduceMultipleErrors()
{ {
$this->expectFailsRule(new NoUndefinedVariables, ' $this->expectFailsRule(new NoUndefinedVariables, '
@ -290,29 +341,27 @@ class NoUndefinedVariablesTest extends TestCase
field2(c: $c) field2(c: $c)
} }
', [ ', [
$this->undefVarByOp('a', 9, 19, 'Foo', 2, 7), $this->undefVar('a', 9, 19, 'Foo', 2, 7),
$this->undefVarByOp('c', 14, 19, 'Foo', 2, 7), $this->undefVar('a', 11, 19, 'Foo', 2, 7),
$this->undefVarByOp('a', 11, 19, 'Foo', 2, 7), $this->undefVar('c', 14, 19, 'Foo', 2, 7),
$this->undefVarByOp('b', 9, 26, 'Bar', 5, 7), $this->undefVar('b', 9, 26, 'Bar', 5, 7),
$this->undefVarByOp('c', 14, 19, 'Bar', 5, 7), $this->undefVar('b', 11, 26, 'Bar', 5, 7),
$this->undefVarByOp('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( $locs = [new SourceLocation($line, $column)];
NoUndefinedVariables::undefinedVarMessage($varName),
[new SourceLocation($line, $column)] if ($l2 && $c2) {
); $locs[] = new SourceLocation($l2, $c2);
} }
private function undefVarByOp($varName, $l1, $c1, $opName, $l2, $c2)
{
return FormattedError::create( return FormattedError::create(
NoUndefinedVariables::undefinedVarByOpMessage($varName, $opName), NoUndefinedVariables::undefinedVarMessage($varName, $opName),
[new SourceLocation($l1, $c1), new SourceLocation($l2, $c2)] $locs
); );
} }
} }

View File

@ -8,6 +8,10 @@ use GraphQL\Validator\Rules\NoUnusedFragments;
class NoUnusedFragmentsTest extends TestCase class NoUnusedFragmentsTest extends TestCase
{ {
// Validate: No unused fragments // Validate: No unused fragments
/**
* @it all fragment names are used
*/
public function testAllFragmentNamesAreUsed() public function testAllFragmentNamesAreUsed()
{ {
$this->expectPassesRule(new NoUnusedFragments(), ' $this->expectPassesRule(new NoUnusedFragments(), '
@ -32,6 +36,9 @@ class NoUnusedFragmentsTest extends TestCase
'); ');
} }
/**
* @it all fragment names are used by multiple operations
*/
public function testAllFragmentNamesAreUsedByMultipleOperations() public function testAllFragmentNamesAreUsedByMultipleOperations()
{ {
$this->expectPassesRule(new NoUnusedFragments, ' $this->expectPassesRule(new NoUnusedFragments, '
@ -58,6 +65,9 @@ class NoUnusedFragmentsTest extends TestCase
'); ');
} }
/**
* @it contains unknown fragments
*/
public function testContainsUnknownFragments() public function testContainsUnknownFragments()
{ {
$this->expectFailsRule(new NoUnusedFragments, ' $this->expectFailsRule(new NoUnusedFragments, '
@ -93,6 +103,9 @@ class NoUnusedFragmentsTest extends TestCase
]); ]);
} }
/**
* @it contains unknown fragments with ref cycle
*/
public function testContainsUnknownFragmentsWithRefCycle() public function testContainsUnknownFragmentsWithRefCycle()
{ {
$this->expectFailsRule(new NoUnusedFragments, ' $this->expectFailsRule(new NoUnusedFragments, '
@ -130,6 +143,9 @@ class NoUnusedFragmentsTest extends TestCase
]); ]);
} }
/**
* @it contains unknown and undef fragments
*/
public function testContainsUnknownAndUndefFragments() public function testContainsUnknownAndUndefFragments()
{ {

View File

@ -3,6 +3,7 @@ namespace GraphQL\Tests\Validator;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
use GraphQL\Schema; use GraphQL\Schema;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType; 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([ $DogCommand = new EnumType([
'name' => 'DogCommand', 'name' => 'DogCommand',
'values' => [ 'values' => [
'SIT' => ['value' => 0], 'SIT' => ['value' => 0],
'HEEL' => ['value' => 1], '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()]] 'args' => ['x' => ['type' => Type::int()], 'y' => ['type' => Type::int()]]
] ]
], ],
'interfaces' => [$Being, $Pet] 'interfaces' => [$Being, $Pet, $Canine]
]); ]);
$Cat = new ObjectType([ $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; return $defaultSchema;
} }