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;
class FragmentDefinition extends Node implements Definition
class FragmentDefinition extends Node implements Definition, HasSelectionSet
{
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
namespace GraphQL\Language\AST;
class OperationDefinition extends Node implements Definition
class OperationDefinition extends Node implements Definition, HasSelectionSet
{
/**
* @var string

View File

@ -2,12 +2,10 @@
namespace GraphQL\Language;
use GraphQL\Language\AST\Node;
use GraphQL\Utils\TypeInfo;
class Visitor
{
const BREAK_VISIT = '@@BREAK@@';
const CONTINUE_VISIT = '@@CONTINUE@@';
/**
* Break visitor
*
@ -244,7 +242,7 @@ class Visitor
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) {
$result = call_user_func($visitFn, $node, $key, $parent, $path, $ancestors);
@ -308,13 +306,100 @@ class Visitor
return $newRoot;
}
/**
* @param $visitors
* @return array
*/
static function visitInParallel($visitors)
{
// TODO: implement real parallel visiting once PHP supports it
$visitorsCount = count($visitors);
$skipping = new \SplFixedArray($visitorsCount);
return [
'enter' => function ($node) use ($visitors, $skipping, $visitorsCount) {
for ($i = 0; $i < $visitorsCount; $i++) {
if (empty($skipping[$i])) {
$fn = self::getVisitFn($visitors[$i], $node->kind, /* isLeaving */ false);
if ($fn) {
$result = call_user_func_array($fn, func_get_args());
if ($result instanceof VisitorOperation) {
if ($result->doContinue) {
$skipping[$i] = $node;
} else if ($result->doBreak) {
$skipping[$i] = $result;
}
} else if ($result !== null) {
return $result;
}
}
}
}
},
'leave' => function ($node) use ($visitors, $skipping, $visitorsCount) {
for ($i = 0; $i < $visitorsCount; $i++) {
if (empty($skipping[$i])) {
$fn = self::getVisitFn($visitors[$i], $node->kind, /* isLeaving */ true);
if ($fn) {
$result = call_user_func_array($fn, func_get_args());
if ($result instanceof VisitorOperation) {
if ($result->doBreak) {
$skipping[$i] = $result;
}
} else if ($result !== null) {
return $result;
}
}
} else if ($skipping[$i] === $node) {
$skipping[$i] = null;
}
}
}
];
}
/**
* Creates a new visitor instance which maintains a provided TypeInfo instance
* along with visiting visitor.
*/
static function visitWithTypeInfo(TypeInfo $typeInfo, $visitor)
{
return [
'enter' => function ($node) use ($typeInfo, $visitor) {
$typeInfo->enter($node);
$fn = self::getVisitFn($visitor, $node->kind, false);
if ($fn) {
$result = call_user_func_array($fn, func_get_args());
if ($result) {
$typeInfo->leave($node);
if ($result instanceof Node) {
$typeInfo->enter($result);
}
}
return $result;
}
return null;
},
'leave' => function ($node) use ($typeInfo, $visitor) {
$fn = self::getVisitFn($visitor, $node->kind, true);
$result = $fn ? call_user_func_array($fn, func_get_args()) : null;
$typeInfo->leave($node);
return $result;
}
];
}
/**
* @param $visitor
* @param $isLeaving
* @param $kind
* @param $isLeaving
* @return null
*/
public static function getVisitFn($visitor, $isLeaving, $kind)
public static function getVisitFn($visitor, $kind, $isLeaving)
{
if (!$visitor) {
return null;

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ use GraphQL\Utils;
class UnionType extends Type implements AbstractType, OutputType, CompositeType
{
/**
* @var Array<GraphQLObjectType>
* @var ObjectType[]
*/
private $_types;
@ -48,10 +48,16 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
$this->_config = $config;
}
/**
* @return array<ObjectType>
*/
public function getPossibleTypes()
{
trigger_error(__METHOD__ . ' is deprecated in favor of ' . __CLASS__ . '::getTypes()', E_USER_DEPRECATED);
return $this->getTypes();
}
/**
* @return ObjectType[]
*/
public function getTypes()
{
if ($this->_types instanceof \Closure) {
$this->_types = call_user_func($this->_types);
@ -71,7 +77,7 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
if (null === $this->_possibleTypeNames) {
$this->_possibleTypeNames = [];
foreach ($this->getPossibleTypes() as $possibleType) {
foreach ($this->getTypes() as $possibleType) {
$this->_possibleTypeNames[$possibleType->name] = true;
}
}

View File

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

View File

@ -8,11 +8,13 @@ use GraphQL\Language\AST\FragmentSpread;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\Value;
use GraphQL\Language\AST\Variable;
use GraphQL\Language\Printer;
use GraphQL\Language\Visitor;
use GraphQL\Language\VisitorOperation;
use GraphQL\Schema;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ScalarType;
@ -27,6 +29,7 @@ use GraphQL\Validator\Rules\KnownArgumentNames;
use GraphQL\Validator\Rules\KnownDirectives;
use GraphQL\Validator\Rules\KnownFragmentNames;
use GraphQL\Validator\Rules\KnownTypeNames;
use GraphQL\Validator\Rules\LoneAnonymousOperation;
use GraphQL\Validator\Rules\NoFragmentCycles;
use GraphQL\Validator\Rules\NoUndefinedVariables;
use GraphQL\Validator\Rules\NoUnusedFragments;
@ -62,28 +65,31 @@ class DocumentValidator
{
if (null === self::$defaultRules) {
self::$defaultRules = [
// new UniqueOperationNames,
// new LoneAnonymousOperation,
// 'UniqueOperationNames' => new UniqueOperationNames(),
'LoneAnonymousOperation' => new LoneAnonymousOperation(),
'KnownTypeNames' => new KnownTypeNames(),
'FragmentsOnCompositeTypes' => new FragmentsOnCompositeTypes(),
'VariablesAreInputTypes' => new VariablesAreInputTypes(),
'ScalarLeafs' => new ScalarLeafs(),
'FieldsOnCorrectType' => new FieldsOnCorrectType(),
// new UniqueFragmentNames,
// 'UniqueFragmentNames' => new UniqueFragmentNames(),
'KnownFragmentNames' => new KnownFragmentNames(),
'NoUnusedFragments' => new NoUnusedFragments(),
'PossibleFragmentSpreads' => new PossibleFragmentSpreads(),
'NoFragmentCycles' => new NoFragmentCycles(),
// 'UniqueVariableNames' => new UniqueVariableNames(),
'NoUndefinedVariables' => new NoUndefinedVariables(),
'NoUnusedVariables' => new NoUnusedVariables(),
'KnownDirectives' => new KnownDirectives(),
'KnownArgumentNames' => new KnownArgumentNames(),
// new UniqueArgumentNames,
// 'UniqueArgumentNames' => new UniqueArgumentNames(),
'ArgumentsOfCorrectType' => new ArgumentsOfCorrectType(),
'ProvidedNonNullArguments' => new ProvidedNonNullArguments(),
'DefaultValuesOfCorrectType' => new DefaultValuesOfCorrectType(),
'VariablesInAllowedPosition' => new VariablesInAllowedPosition(),
'OverlappingFieldsCanBeMerged' => new OverlappingFieldsCanBeMerged(),
// 'UniqueInputFieldNames' => new UniqueInputFieldNames(),
// Query Security
'QueryDepth' => new QueryDepth(QueryDepth::DISABLED), // default disabled
'QueryComplexity' => new QueryComplexity(QueryComplexity::DISABLED), // default disabled
@ -107,7 +113,8 @@ class DocumentValidator
public static function validate(Schema $schema, Document $ast, array $rules = null)
{
$errors = static::visitUsingRules($schema, $ast, $rules ?: static::allRules());
$typeInfo = new TypeInfo($schema);
$errors = static::visitUsingRules($schema, $typeInfo, $ast, $rules ?: static::allRules());
return $errors;
}
@ -128,76 +135,109 @@ class DocumentValidator
return $arr;
}
public static function isValidLiteralValue($valueAST, Type $type)
/**
* Utility for validators which determines if a value literal AST is valid given
* an input type.
*
* Note that this only validates literal values, variables are assumed to
* provide values of the correct type.
*
* @return array
*/
public static function isValidLiteralValue(Type $type, $valueAST)
{
// A value can only be not provided if the type is nullable.
if (!$valueAST) {
return !($type instanceof NonNull);
// A value must be provided if the type is non-null.
if ($type instanceof NonNull) {
$wrappedType = $type->getWrappedType();
if (!$valueAST) {
if ($wrappedType->name) {
return [ "Expected \"{$wrappedType->name}!\", found null." ];
}
return ['Expected non-null value, found null.'];
}
return static::isValidLiteralValue($wrappedType, $valueAST);
}
// Unwrap non-null.
if ($type instanceof NonNull) {
return static::isValidLiteralValue($valueAST, $type->getWrappedType());
if (!$valueAST) {
return [];
}
// This function only tests literals, and assumes variables will provide
// values of the correct type.
if ($valueAST instanceof Variable) {
return true;
}
if (!$valueAST instanceof Value) {
return false;
return [];
}
// Lists accept a non-list value as a list of one.
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
if ($valueAST instanceof ListValue) {
foreach($valueAST->values as $itemAST) {
if (!static::isValidLiteralValue($itemAST, $itemType)) {
return false;
$errors = [];
foreach($valueAST->values as $index => $itemAST) {
$tmp = static::isValidLiteralValue($itemType, $itemAST);
if ($tmp) {
$errors = array_merge($errors, Utils::map($tmp, function($error) use ($index) {
return "In element #$index: $error";
}));
}
}
return true;
return $errors;
} else {
return static::isValidLiteralValue($valueAST, $itemType);
return static::isValidLiteralValue($itemType, $valueAST);
}
}
// Scalar/Enum input checks to ensure the type can serialize the value to
// a non-null value.
if ($type instanceof ScalarType || $type instanceof EnumType) {
return $type->parseLiteral($valueAST) !== null;
}
// Input objects check each defined field, ensuring it is of the correct
// type and provided if non-nullable.
// Input objects check each defined field and look for undefined fields.
if ($type instanceof InputObjectType) {
$fields = $type->getFields();
if ($valueAST->kind !== Node::OBJECT) {
return false;
return [ "Expected \"{$type->name}\", found not an object." ];
}
$fieldASTs = $valueAST->fields;
$fieldASTMap = Utils::keyMap($fieldASTs, function($field) {return $field->name->value;});
foreach ($fields as $fieldKey => $field) {
$fieldName = $field->name ?: $fieldKey;
if (!isset($fieldASTMap[$fieldName]) && $field->getType() instanceof NonNull) {
// Required fields missing
return false;
$fields = $type->getFields();
$errors = [];
// Ensure every provided field is defined.
$fieldASTs = $valueAST->fields;
foreach ($fieldASTs as $providedFieldAST) {
if (empty($fields[$providedFieldAST->name->value])) {
$errors[] = "In field \"{$providedFieldAST->name->value}\": Unknown field.";
}
}
foreach ($fieldASTs as $fieldAST) {
if (empty($fields[$fieldAST->name->value]) || !static::isValidLiteralValue($fieldAST->value, $fields[$fieldAST->name->value]->getType())) {
return false;
// Ensure every defined field is valid.
$fieldASTMap = Utils::keyMap($fieldASTs, function($fieldAST) {return $fieldAST->name->value;});
foreach ($fields as $fieldName => $field) {
$result = static::isValidLiteralValue(
$field->getType(),
isset($fieldASTMap[$fieldName]) ? $fieldASTMap[$fieldName]->value : null
);
if ($result) {
$errors = array_merge($errors, Utils::map($result, function($error) use ($fieldName) {
return "In field \"$fieldName\": $error";
}));
}
}
return true;
return $errors;
}
// Any other kind of type is not an input type, and a literal cannot be used.
return false;
Utils::invariant(
$type instanceof ScalarType || $type instanceof EnumType,
'Must be input type'
);
// Scalar/Enum input checks to ensure the type can parse the value to
// a non-null value.
$parseResult = $type->parseLiteral($valueAST);
if (null === $parseResult) {
$printed = Printer::doPrint($valueAST);
return [ "Expected type \"{$type->name}\", found $printed." ];
}
return [];
}
/**
@ -205,14 +245,23 @@ class DocumentValidator
* while maintaining the visitor skip and break API.
*
* @param Schema $schema
* @param TypeInfo $typeInfo
* @param Document $documentAST
* @param array $rules
* @return array
*/
public static function visitUsingRules(Schema $schema, Document $documentAST, array $rules)
public static function visitUsingRules(Schema $schema, TypeInfo $typeInfo, Document $documentAST, array $rules)
{
$typeInfo = new TypeInfo($schema);
$context = new ValidationContext($schema, $documentAST, $typeInfo);
$visitors = [];
foreach ($rules as $rule) {
$visitors[] = $rule($context);
}
Visitor::visit($documentAST, Visitor::visitWithTypeInfo($typeInfo, Visitor::visitInParallel($visitors)));
return $context->getErrors();
$errors = [];
// TODO: convert to class
@ -237,7 +286,7 @@ class DocumentValidator
if ($node->kind === Node::FRAGMENT_DEFINITION && $key !== null && !empty($instances[$i]['visitSpreadFragments'])) {
$result = Visitor::skipNode();
} else {
$enter = Visitor::getVisitFn($instances[$i], false, $node->kind);
$enter = Visitor::getVisitFn($instances[$i], $node->kind, false);
if ($enter instanceof \Closure) {
// $enter = $enter->bindTo($instances[$i]);
$result = call_user_func_array($enter, func_get_args());
@ -266,7 +315,7 @@ class DocumentValidator
} else if ($result && static::isError($result)) {
static::append($errors, $result);
for ($j = $i - 1; $j >= 0; $j--) {
$leaveFn = Visitor::getVisitFn($instances[$j], true, $node->kind);
$leaveFn = Visitor::getVisitFn($instances[$j], $node->kind, true);
if ($leaveFn) {
// $leaveFn = $leaveFn->bindTo($instances[$j])
$result = call_user_func_array($leaveFn, func_get_args());
@ -316,7 +365,7 @@ class DocumentValidator
}
continue;
}
$leaveFn = Visitor::getVisitFn($instances[$i], true, $node->kind);
$leaveFn = Visitor::getVisitFn($instances[$i], $node->kind, true);
if ($leaveFn) {
// $leaveFn = $leaveFn.bindTo($instances[$i]);

View File

@ -1,6 +1,8 @@
<?php
namespace GraphQL\Validator;
use GraphQL\Utils;
class Messages
{
static function missingArgMessage($fieldName, $argName, $typeName)
@ -13,23 +15,6 @@ class Messages
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)
{
return "Fragment $fragName cannot condition on non composite type \"$typeName\".";

View File

@ -16,9 +16,10 @@ use GraphQL\Validator\ValidationContext;
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)
@ -26,12 +27,17 @@ class ArgumentsOfCorrectType
return [
Node::ARGUMENT => function(Argument $argAST) use ($context) {
$argDef = $context->getArgument();
if ($argDef && !DocumentValidator::isValidLiteralValue($argAST->value, $argDef->getType())) {
return new Error(
self::badValueMessage($argAST->name->value, $argDef->getType(), Printer::doPrint($argAST->value)),
[$argAST->value]
);
if ($argDef) {
$errors = DocumentValidator::isValidLiteralValue($argDef->getType(), $argAST->value);
if (!empty($errors)) {
$context->reportError(new Error(
self::badValueMessage($argAST->name->value, $argDef->getType(), Printer::doPrint($argAST->value), $errors),
[$argAST->value]
));
}
}
return Visitor::skipNode();
}
];
}

View File

@ -13,6 +13,19 @@ use GraphQL\Validator\ValidationContext;
class DefaultValuesOfCorrectType
{
static function badValueForDefaultArgMessage($varName, $type, $value, $verboseErrors = null)
{
$message = $verboseErrors ? ("\n" . implode("\n", $verboseErrors)) : '';
return "Variable \$$varName has invalid default value: $value.$message";
}
static function defaultForNonNullArgMessage($varName, $type, $guessType)
{
return "Variable \$$varName of type $type " .
"is required and will never use the default value. " .
"Perhaps you meant to use type $guessType.";
}
public function __invoke(ValidationContext $context)
{
return [
@ -22,16 +35,19 @@ class DefaultValuesOfCorrectType
$type = $context->getInputType();
if ($type instanceof NonNull && $defaultValue) {
return new Error(
Messages::defaultForNonNullArgMessage($name, $type, $type->getWrappedType()),
$context->reportError(new Error(
static::defaultForNonNullArgMessage($name, $type, $type->getWrappedType()),
[$defaultValue]
);
));
}
if ($type && $defaultValue && !DocumentValidator::isValidLiteralValue($defaultValue, $type)) {
return new Error(
Messages::badValueForDefaultArgMessage($name, $type, Printer::doPrint($defaultValue)),
[$defaultValue]
);
if ($type && $defaultValue) {
$errors = DocumentValidator::isValidLiteralValue($type, $defaultValue);
if (!empty($errors)) {
$context->reportError(new Error(
static::badValueForDefaultArgMessage($name, $type, Printer::doPrint($defaultValue), $errors),
[$defaultValue]
));
}
}
return null;
}

View File

@ -5,11 +5,34 @@ namespace GraphQL\Validator\Rules;
use GraphQL\Error;
use GraphQL\Language\AST\Field;
use GraphQL\Language\AST\Node;
use GraphQL\Schema;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Utils;
use GraphQL\Validator\Messages;
use GraphQL\Validator\ValidationContext;
class FieldsOnCorrectType
{
static function undefinedFieldMessage($field, $type, array $suggestedTypes = [])
{
$message = 'Cannot query field "' . $field . '" on type "' . $type.'".';
$maxLength = 5;
$count = count($suggestedTypes);
if ($count > 0) {
$suggestions = array_slice($suggestedTypes, 0, $maxLength);
$suggestions = Utils::map($suggestions, function($t) { return "\"$t\""; });
$suggestions = implode(', ', $suggestions);
if ($count > $maxLength) {
$suggestions .= ', and ' . ($count - $maxLength) . ' other types';
}
$message .= " However, this field exists on $suggestions.";
$message .= ' Perhaps you meant to use an inline fragment?';
}
return $message;
}
public function __invoke(ValidationContext $context)
{
return [
@ -18,13 +41,71 @@ class FieldsOnCorrectType
if ($type) {
$fieldDef = $context->getFieldDef();
if (!$fieldDef) {
return new Error(
Messages::undefinedFieldMessage($node->name->value, $type->name),
// This isn't valid. Let's find suggestions, if any.
$suggestedTypes = [];
if ($type instanceof AbstractType) {
$schema = $context->getSchema();
$suggestedTypes = self::getSiblingInterfacesIncludingField(
$schema,
$type,
$node->name->value
);
$suggestedTypes = array_merge($suggestedTypes,
self::getImplementationsIncludingField($schema, $type, $node->name->value)
);
}
$context->reportError(new Error(
static::undefinedFieldMessage($node->name->value, $type->name, $suggestedTypes),
[$node]
);
));
}
}
}
];
}
/**
* Return implementations of `type` that include `fieldName` as a valid field.
*
* @param Schema $schema
* @param AbstractType $type
* @param $fieldName
* @return array
*/
static function getImplementationsIncludingField(Schema $schema, AbstractType $type, $fieldName)
{
$types = $schema->getPossibleTypes($type);
$types = Utils::filter($types, function($t) use ($fieldName) {return isset($t->getFields()[$fieldName]);});
$types = Utils::map($types, function($t) {return $t->name;});
sort($types);
return $types;
}
/**
* Go through all of the implementations of type, and find other interaces
* that they implement. If those interfaces include `field` as a valid field,
* return them, sorted by how often the implementations include the other
* interface.
*/
static function getSiblingInterfacesIncludingField(Schema $schema, AbstractType $type, $fieldName)
{
$types = $schema->getPossibleTypes($type);
$suggestedInterfaces = array_reduce($types, function ($acc, $t) use ($fieldName) {
foreach ($t->getInterfaces() as $i) {
if (empty($i->getFields()[$fieldName])) {
continue;
}
if (!isset($acc[$i->name])) {
$acc[$i->name] = 0;
}
$acc[$i->name] += 1;
}
return $acc;
}, []);
$suggestedInterfaceNames = array_keys($suggestedInterfaces);
usort($suggestedInterfaceNames, function($a, $b) use ($suggestedInterfaces) {
return $suggestedInterfaces[$b] - $suggestedInterfaces[$a];
});
return $suggestedInterfaceNames;
}
}

View File

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

View File

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

View File

@ -12,6 +12,7 @@ use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Validator\Messages;
use GraphQL\Validator\ValidationContext;
use GraphQL\Type\Definition\Directive as DirectiveDef;
class KnownDirectives
{
@ -20,9 +21,9 @@ class KnownDirectives
return "Unknown directive \"$directiveName\".";
}
static function misplacedDirectiveMessage($directiveName, $placement)
static function misplacedDirectiveMessage($directiveName, $location)
{
return "Directive \"$directiveName\" may not be used on \"$placement\".";
return "Directive \"$directiveName\" may not be used on \"$location\".";
}
public function __invoke(ValidationContext $context)
@ -38,39 +39,44 @@ class KnownDirectives
}
if (!$directiveDef) {
return new Error(
$context->reportError(new Error(
self::unknownDirectiveMessage($node->name->value),
[$node]
);
));
return ;
}
$appliedTo = $ancestors[count($ancestors) - 1];
$candidateLocation = $this->getLocationForAppliedNode($appliedTo);
if ($appliedTo instanceof OperationDefinition && !$directiveDef->onOperation) {
return new Error(
self::misplacedDirectiveMessage($node->name->value, 'operation'),
if (!$candidateLocation) {
$context->reportError(new Error(
self::misplacedDirectiveMessage($node->name->value, $node->type),
[$node]
);
}
if ($appliedTo instanceof Field && !$directiveDef->onField) {
return new Error(
self::misplacedDirectiveMessage($node->name->value, 'field'),
[$node]
);
}
$fragmentKind = (
$appliedTo instanceof FragmentSpread ||
$appliedTo instanceof InlineFragment ||
$appliedTo instanceof FragmentDefinition
);
if ($fragmentKind && !$directiveDef->onFragment) {
return new Error(
self::misplacedDirectiveMessage($node->name->value, 'fragment'),
[$node]
);
));
} else if (!in_array($candidateLocation, $directiveDef->locations)) {
$context->reportError(new Error(
self::misplacedDirectiveMessage($node->name->value, $candidateLocation),
[ $node ]
));
}
}
];
}
private function getLocationForAppliedNode(Node $appliedTo)
{
switch ($appliedTo->kind) {
case Node::OPERATION_DEFINITION:
switch ($appliedTo->operation) {
case 'query': return DirectiveDef::$directiveLocations['QUERY'];
case 'mutation': return DirectiveDef::$directiveLocations['MUTATION'];
case 'subscription': return DirectiveDef::$directiveLocations['SUBSCRIPTION'];
}
break;
case Node::FIELD: return DirectiveDef::$directiveLocations['FIELD'];
case Node::FRAGMENT_SPREAD: return DirectiveDef::$directiveLocations['FRAGMENT_SPREAD'];
case Node::INLINE_FRAGMENT: return DirectiveDef::$directiveLocations['INLINE_FRAGMENT'];
case Node::FRAGMENT_DEFINITION: return DirectiveDef::$directiveLocations['FRAGMENT_DEFINITION'];
}
}
}

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,13 @@
<?php
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\Language\AST\Document;
use GraphQL\Language\AST\FragmentDefinition;
@ -33,16 +41,69 @@ class ValidationContext
*/
private $_typeInfo;
/**
* @var Error[]
*/
private $_errors;
/**
* @var array<string, FragmentDefinition>
*/
private $_fragments;
/**
* @var SplObjectStorage
*/
private $_fragmentSpreads;
/**
* @var SplObjectStorage
*/
private $_recursivelyReferencedFragments;
/**
* @var SplObjectStorage
*/
private $_variableUsages;
/**
* @var SplObjectStorage
*/
private $_recursiveVariableUsages;
/**
* ValidationContext constructor.
*
* @param Schema $schema
* @param Document $ast
* @param TypeInfo $typeInfo
*/
function __construct(Schema $schema, Document $ast, TypeInfo $typeInfo)
{
$this->_schema = $schema;
$this->_ast = $ast;
$this->_typeInfo = $typeInfo;
$this->_errors = [];
$this->_fragmentSpreads = new SplObjectStorage();
$this->_recursivelyReferencedFragments = new SplObjectStorage();
$this->_variableUsages = new SplObjectStorage();
$this->_recursiveVariableUsages = new SplObjectStorage();
}
/**
* @param Error $error
*/
function reportError(Error $error)
{
$this->_errors[] = $error;
}
/**
* @return Error[]
*/
function getErrors()
{
return $this->_errors;
}
/**
@ -80,6 +141,113 @@ class ValidationContext
return isset($fragments[$name]) ? $fragments[$name] : null;
}
/**
* @param HasSelectionSet $node
* @return FragmentSpread[]
*/
function getFragmentSpreads(HasSelectionSet $node)
{
$spreads = isset($this->_fragmentSpreads[$node]) ? $this->_fragmentSpreads[$node] : null;
if (!$spreads) {
$spreads = [];
$setsToVisit = [$node->selectionSet];
while (!empty($setsToVisit)) {
$set = array_pop($setsToVisit);
for ($i = 0; $i < count($set->selections); $i++) {
$selection = $set->selections[$i];
if ($selection->kind === Node::FRAGMENT_SPREAD) {
$spreads[] = $selection;
} else if ($selection->selectionSet) {
$setsToVisit[] = $selection->selectionSet;
}
}
}
$this->_fragmentSpreads[$node] = $spreads;
}
return $spreads;
}
/**
* @param OperationDefinition $operation
* @return FragmentDefinition[]
*/
function getRecursivelyReferencedFragments(OperationDefinition $operation)
{
$fragments = isset($this->_recursivelyReferencedFragments[$operation]) ? $this->_recursivelyReferencedFragments[$operation] : null;
if (!$fragments) {
$fragments = [];
$collectedNames = [];
$nodesToVisit = [$operation];
while (!empty($nodesToVisit)) {
$node = array_pop($nodesToVisit);
$spreads = $this->getFragmentSpreads($node);
for ($i = 0; $i < count($spreads); $i++) {
$fragName = $spreads[$i]->name->value;
if (empty($collectedNames[$fragName])) {
$collectedNames[$fragName] = true;
$fragment = $this->getFragment($fragName);
if ($fragment) {
$fragments[] = $fragment;
$nodesToVisit[] = $fragment;
}
}
}
}
$this->_recursivelyReferencedFragments[$operation] = $fragments;
}
return $fragments;
}
/**
* @param HasSelectionSet $node
* @return array List of ['node' => Variable, 'type' => ?InputObjectType]
*/
function getVariableUsages(HasSelectionSet $node)
{
$usages = isset($this->_variableUsages[$node]) ? $this->_variableUsages[$node] : null;
if (!$usages) {
$newUsages = [];
$typeInfo = new TypeInfo($this->_schema);
Visitor::visit($node, Visitor::visitWithTypeInfo($typeInfo, [
Node::VARIABLE_DEFINITION => function () {
return false;
},
Node::VARIABLE => function (Variable $variable) use (&$newUsages, $typeInfo) {
$newUsages[] = ['node' => $variable, 'type' => $typeInfo->getInputType()];
}
]));
$usages = $newUsages;
$this->_variableUsages[$node] = $usages;
}
return $usages;
}
/**
* @param OperationDefinition $operation
* @return array List of ['node' => Variable, 'type' => ?InputObjectType]
*/
function getRecursiveVariableUsages(OperationDefinition $operation)
{
$usages = isset($this->_recursiveVariableUsages[$operation]) ? $this->_recursiveVariableUsages[$operation] : null;
if (!$usages) {
$usages = $this->getVariableUsages($operation);
$fragments = $this->getRecursivelyReferencedFragments($operation);
$tmp = [$usages];
for ($i = 0; $i < count($fragments); $i++) {
$tmp[] = $this->getVariableUsages($fragments[$i]);
}
$usages = call_user_func_array('array_merge', $tmp);
$this->_recursiveVariableUsages[$operation] = $usages;
}
return $usages;
}
/**
* Returns OutputType
*

View File

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

View File

@ -10,6 +10,9 @@ class DefaultValuesOfCorrectTypeTest extends TestCase
{
// Validate: Variable default values of correct type
/**
* @it variables with no default values
*/
public function testVariablesWithNoDefaultValues()
{
$this->expectPassesRule(new DefaultValuesOfCorrectType, '
@ -19,6 +22,9 @@ class DefaultValuesOfCorrectTypeTest extends TestCase
');
}
/**
* @it required variables without default values
*/
public function testRequiredVariablesWithoutDefaultValues()
{
$this->expectPassesRule(new DefaultValuesOfCorrectType, '
@ -28,6 +34,9 @@ class DefaultValuesOfCorrectTypeTest extends TestCase
');
}
/**
* @it variables with valid default values
*/
public function testVariablesWithValidDefaultValues()
{
$this->expectPassesRule(new DefaultValuesOfCorrectType, '
@ -41,6 +50,9 @@ class DefaultValuesOfCorrectTypeTest extends TestCase
');
}
/**
* @it no required variables with default values
*/
public function testNoRequiredVariablesWithDefaultValues()
{
$this->expectFailsRule(new DefaultValuesOfCorrectType, '
@ -53,6 +65,9 @@ class DefaultValuesOfCorrectTypeTest extends TestCase
]);
}
/**
* @it variables with invalid default values
*/
public function testVariablesWithInvalidDefaultValues()
{
$this->expectFailsRule(new DefaultValuesOfCorrectType, '
@ -64,12 +79,21 @@ class DefaultValuesOfCorrectTypeTest extends TestCase
dog { name }
}
', [
$this->badValue('a', 'Int', '"one"', 3, 19),
$this->badValue('b', 'String', '4', 4, 22),
$this->badValue('c', 'ComplexInput', '"notverycomplex"', 5, 28)
$this->badValue('a', 'Int', '"one"', 3, 19, [
'Expected type "Int", found "one".'
]),
$this->badValue('b', 'String', '4', 4, 22, [
'Expected type "String", found 4.'
]),
$this->badValue('c', 'ComplexInput', '"notverycomplex"', 5, 28, [
'Expected "ComplexInput", found not an object.'
])
]);
}
/**
* @it complex variables missing required field
*/
public function testComplexVariablesMissingRequiredField()
{
$this->expectFailsRule(new DefaultValuesOfCorrectType, '
@ -77,10 +101,15 @@ class DefaultValuesOfCorrectTypeTest extends TestCase
dog { name }
}
', [
$this->badValue('a', 'ComplexInput', '{intField: 3}', 2, 53)
$this->badValue('a', 'ComplexInput', '{intField: 3}', 2, 53, [
'In field "requiredField": Expected "Boolean!", found null.'
])
]);
}
/**
* @it list variables with invalid item
*/
public function testListVariablesWithInvalidItem()
{
$this->expectFailsRule(new DefaultValuesOfCorrectType, '
@ -88,22 +117,26 @@ class DefaultValuesOfCorrectTypeTest extends TestCase
dog { name }
}
', [
$this->badValue('a', '[String]', '["one", 2]', 2, 40)
$this->badValue('a', '[String]', '["one", 2]', 2, 40, [
'In element #1: Expected type "String", found 2.'
])
]);
}
private function defaultForNonNullArg($varName, $typeName, $guessTypeName, $line, $column)
{
return FormattedError::create(
Messages::defaultForNonNullArgMessage($varName, $typeName, $guessTypeName),
DefaultValuesOfCorrectType::defaultForNonNullArgMessage($varName, $typeName, $guessTypeName),
[ new SourceLocation($line, $column) ]
);
}
private function badValue($varName, $typeName, $val, $line, $column)
private function badValue($varName, $typeName, $val, $line, $column, $errors = null)
{
$realErrors = !$errors ? ["Expected type \"$typeName\", found $val."] : $errors;
return FormattedError::create(
Messages::badValueForDefaultArgMessage($varName, $typeName, $val),
DefaultValuesOfCorrectType::badValueForDefaultArgMessage($varName, $typeName, $val, $realErrors),
[ new SourceLocation($line, $column) ]
);
}

View File

@ -9,6 +9,10 @@ use GraphQL\Validator\Rules\FieldsOnCorrectType;
class FieldsOnCorrectTypeTest extends TestCase
{
// Validate: Fields on correct type
/**
* @it Object field selection
*/
public function testObjectFieldSelection()
{
$this->expectPassesRule(new FieldsOnCorrectType(), '
@ -19,6 +23,9 @@ class FieldsOnCorrectTypeTest extends TestCase
');
}
/**
* @it Aliased object field selection
*/
public function testAliasedObjectFieldSelection()
{
$this->expectPassesRule(new FieldsOnCorrectType, '
@ -29,6 +36,9 @@ class FieldsOnCorrectTypeTest extends TestCase
');
}
/**
* @it Interface field selection
*/
public function testInterfaceFieldSelection()
{
$this->expectPassesRule(new FieldsOnCorrectType, '
@ -39,6 +49,9 @@ class FieldsOnCorrectTypeTest extends TestCase
');
}
/**
* @it Aliased interface field selection
*/
public function testAliasedInterfaceFieldSelection()
{
$this->expectPassesRule(new FieldsOnCorrectType, '
@ -48,6 +61,9 @@ class FieldsOnCorrectTypeTest extends TestCase
');
}
/**
* @it Lying alias selection
*/
public function testLyingAliasSelection()
{
$this->expectPassesRule(new FieldsOnCorrectType, '
@ -57,6 +73,9 @@ class FieldsOnCorrectTypeTest extends TestCase
');
}
/**
* @it Ignores fields on unknown type
*/
public function testIgnoresFieldsOnUnknownType()
{
$this->expectPassesRule(new FieldsOnCorrectType, '
@ -66,17 +85,41 @@ class FieldsOnCorrectTypeTest extends TestCase
');
}
/**
* @it reports errors when type is known again
*/
public function testReportsErrorsWhenTypeIsKnownAgain()
{
$this->expectFailsRule(new FieldsOnCorrectType, '
fragment typeKnownAgain on Pet {
unknown_pet_field {
... on Cat {
unknown_cat_field
}
}
}',
[ $this->undefinedField('unknown_pet_field', 'Pet', [], 3, 9),
$this->undefinedField('unknown_cat_field', 'Cat', [], 5, 13) ]
);
}
/**
* @it Field not defined on fragment
*/
public function testFieldNotDefinedOnFragment()
{
$this->expectFailsRule(new FieldsOnCorrectType, '
fragment fieldNotDefined on Dog {
meowVolume
}',
[$this->undefinedField('meowVolume', 'Dog', 3, 9)]
[$this->undefinedField('meowVolume', 'Dog', [], 3, 9)]
);
}
public function testFieldNotDefinedDeeplyOnlyReportsFirst()
/**
* @it Ignores deeply unknown field
*/
public function testIgnoresDeeplyUnknownField()
{
$this->expectFailsRule(new FieldsOnCorrectType, '
fragment deepFieldNotDefined on Dog {
@ -84,10 +127,13 @@ class FieldsOnCorrectTypeTest extends TestCase
deeper_unknown_field
}
}',
[$this->undefinedField('unknown_field', 'Dog', 3, 9)]
[$this->undefinedField('unknown_field', 'Dog', [], 3, 9)]
);
}
/**
* @it Sub-field not defined
*/
public function testSubFieldNotDefined()
{
$this->expectFailsRule(new FieldsOnCorrectType, '
@ -96,10 +142,13 @@ class FieldsOnCorrectTypeTest extends TestCase
unknown_field
}
}',
[$this->undefinedField('unknown_field', 'Pet', 4, 11)]
[$this->undefinedField('unknown_field', 'Pet', [], 4, 11)]
);
}
/**
* @it Field not defined on inline fragment
*/
public function testFieldNotDefinedOnInlineFragment()
{
$this->expectFailsRule(new FieldsOnCorrectType, '
@ -108,50 +157,65 @@ class FieldsOnCorrectTypeTest extends TestCase
meowVolume
}
}',
[$this->undefinedField('meowVolume', 'Dog', 4, 11)]
[$this->undefinedField('meowVolume', 'Dog', [], 4, 11)]
);
}
/**
* @it Aliased field target not defined
*/
public function testAliasedFieldTargetNotDefined()
{
$this->expectFailsRule(new FieldsOnCorrectType, '
fragment aliasedFieldTargetNotDefined on Dog {
volume : mooVolume
}',
[$this->undefinedField('mooVolume', 'Dog', 3, 9)]
[$this->undefinedField('mooVolume', 'Dog', [], 3, 9)]
);
}
/**
* @it Aliased lying field target not defined
*/
public function testAliasedLyingFieldTargetNotDefined()
{
$this->expectFailsRule(new FieldsOnCorrectType, '
fragment aliasedLyingFieldTargetNotDefined on Dog {
barkVolume : kawVolume
}',
[$this->undefinedField('kawVolume', 'Dog', 3, 9)]
[$this->undefinedField('kawVolume', 'Dog', [], 3, 9)]
);
}
/**
* @it Not defined on interface
*/
public function testNotDefinedOnInterface()
{
$this->expectFailsRule(new FieldsOnCorrectType, '
fragment notDefinedOnInterface on Pet {
tailLength
}',
[$this->undefinedField('tailLength', 'Pet', 3, 9)]
[$this->undefinedField('tailLength', 'Pet', [], 3, 9)]
);
}
/**
* @it Defined on implementors but not on interface
*/
public function testDefinedOnImplmentorsButNotOnInterface()
{
$this->expectFailsRule(new FieldsOnCorrectType, '
fragment definedOnImplementorsButNotInterface on Pet {
nickname
}',
[$this->undefinedField('nickname', 'Pet', 3, 9)]
[$this->undefinedField('nickname', 'Pet', [ 'Cat', 'Dog' ], 3, 9)]
);
}
/**
* @it Meta field selection on union
*/
public function testMetaFieldSelectionOnUnion()
{
$this->expectPassesRule(new FieldsOnCorrectType, '
@ -161,26 +225,35 @@ class FieldsOnCorrectTypeTest extends TestCase
);
}
/**
* @it Direct field selection on union
*/
public function testDirectFieldSelectionOnUnion()
{
$this->expectFailsRule(new FieldsOnCorrectType, '
fragment directFieldSelectionOnUnion on CatOrDog {
directField
}',
[$this->undefinedField('directField', 'CatOrDog', 3, 9)]
[$this->undefinedField('directField', 'CatOrDog', [], 3, 9)]
);
}
/**
* @it Defined on implementors queried on union
*/
public function testDefinedOnImplementorsQueriedOnUnion()
{
$this->expectFailsRule(new FieldsOnCorrectType, '
fragment definedOnImplementorsQueriedOnUnion on CatOrDog {
name
}',
[$this->undefinedField('name', 'CatOrDog', 3, 9)]
[$this->undefinedField('name', 'CatOrDog', [ 'Being', 'Pet', 'Canine', 'Cat', 'Dog' ], 3, 9)]
);
}
/**
* @it valid field in inline fragment
*/
public function testValidFieldInInlineFragment()
{
$this->expectPassesRule(new FieldsOnCorrectType, '
@ -192,10 +265,45 @@ class FieldsOnCorrectTypeTest extends TestCase
');
}
private function undefinedField($field, $type, $line, $column)
// Describe: Fields on correct type error message
/**
* @it Works with no suggestions
*/
public function testWorksWithNoSuggestions()
{
$this->assertEquals('Cannot query field "T" on type "f".', FieldsOnCorrectType::undefinedFieldMessage('T', 'f', []));
}
/**
* @it Works with no small numbers of suggestions
*/
public function testWorksWithNoSmallNumbersOfSuggestions()
{
$expected = 'Cannot query field "T" on type "f". ' .
'However, this field exists on "A", "B". ' .
'Perhaps you meant to use an inline fragment?';
$this->assertEquals($expected, FieldsOnCorrectType::undefinedFieldMessage('T', 'f', [ 'A', 'B' ]));
}
/**
* @it Works with lots of suggestions
*/
public function testWorksWithLotsOfSuggestions()
{
$expected = 'Cannot query field "T" on type "f". ' .
'However, this field exists on "A", "B", "C", "D", "E", ' .
'and 1 other types. ' .
'Perhaps you meant to use an inline fragment?';
$this->assertEquals($expected, FieldsOnCorrectType::undefinedFieldMessage('T', 'f', [ 'A', 'B', 'C', 'D', 'E', 'F' ]));
}
private function undefinedField($field, $type, $suggestions, $line, $column)
{
return FormattedError::create(
Messages::undefinedFieldMessage($field, $type),
FieldsOnCorrectType::undefinedFieldMessage($field, $type, $suggestions),
[new SourceLocation($line, $column)]
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,9 @@ class KnownTypeNamesTest extends TestCase
{
// Validate: Known type names
/**
* @it known type names are valid
*/
public function testKnownTypeNamesAreValid()
{
$this->expectPassesRule(new KnownTypeNames, '
@ -23,6 +26,9 @@ class KnownTypeNamesTest extends TestCase
');
}
/**
* @it unknown type names are invalid
*/
public function testUnknownTypeNamesAreInvalid()
{
$this->expectFailsRule(new KnownTypeNames, '
@ -42,6 +48,32 @@ class KnownTypeNamesTest extends TestCase
]);
}
/**
* @it ignores type definitions
*/
public function testIgnoresTypeDefinitions()
{
$this->expectFailsRule(new KnownTypeNames, '
type NotInTheSchema {
field: FooBar
}
interface FooBar {
field: NotInTheSchema
}
union U = A | B
input Blob {
field: UnknownType
}
query Foo($var: NotInTheSchema) {
user(id: $var) {
id
}
}
', [
$this->unknownType('NotInTheSchema', 12, 23),
]);
}
private function unknownType($typeName, $line, $column)
{
return FormattedError::create(

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
/**
* @it single reference is valid
*/
public function testSingleReferenceIsValid()
{
$this->expectPassesRule(new NoFragmentCycles(), '
@ -17,6 +20,9 @@ class NoFragmentCyclesTest extends TestCase
');
}
/**
* @it spreading twice is not circular
*/
public function testSpreadingTwiceIsNotCircular()
{
$this->expectPassesRule(new NoFragmentCycles, '
@ -25,6 +31,9 @@ class NoFragmentCyclesTest extends TestCase
');
}
/**
* @it spreading twice indirectly is not circular
*/
public function testSpreadingTwiceIndirectlyIsNotCircular()
{
$this->expectPassesRule(new NoFragmentCycles, '
@ -34,6 +43,9 @@ class NoFragmentCyclesTest extends TestCase
');
}
/**
* @it double spread within abstract types
*/
public function testDoubleSpreadWithinAbstractTypes()
{
$this->expectPassesRule(new NoFragmentCycles, '
@ -49,6 +61,21 @@ class NoFragmentCyclesTest extends TestCase
');
}
/**
* @it does not false positive on unknown fragment
*/
public function testDoesNotFalsePositiveOnUnknownFragment()
{
$this->expectPassesRule(new NoFragmentCycles, '
fragment nameFragment on Pet {
...UnknownFragment
}
');
}
/**
* @it spreading recursively within field fails
*/
public function testSpreadingRecursivelyWithinFieldFails()
{
$this->expectFailsRule(new NoFragmentCycles, '
@ -58,6 +85,9 @@ class NoFragmentCyclesTest extends TestCase
]);
}
/**
* @it no spreading itself directly
*/
public function testNoSpreadingItselfDirectly()
{
$this->expectFailsRule(new NoFragmentCycles, '
@ -67,6 +97,9 @@ class NoFragmentCyclesTest extends TestCase
]);
}
/**
* @it no spreading itself directly within inline fragment
*/
public function testNoSpreadingItselfDirectlyWithinInlineFragment()
{
$this->expectFailsRule(new NoFragmentCycles, '
@ -80,6 +113,9 @@ class NoFragmentCyclesTest extends TestCase
]);
}
/**
* @it no spreading itself indirectly
*/
public function testNoSpreadingItselfIndirectly()
{
$this->expectFailsRule(new NoFragmentCycles, '
@ -93,6 +129,9 @@ class NoFragmentCyclesTest extends TestCase
]);
}
/**
* @it no spreading itself indirectly reports opposite order
*/
public function testNoSpreadingItselfIndirectlyReportsOppositeOrder()
{
$this->expectFailsRule(new NoFragmentCycles, '
@ -106,6 +145,9 @@ class NoFragmentCyclesTest extends TestCase
]);
}
/**
* @it no spreading itself indirectly within inline fragment
*/
public function testNoSpreadingItselfIndirectlyWithinInlineFragment()
{
$this->expectFailsRule(new NoFragmentCycles, '
@ -127,6 +169,9 @@ class NoFragmentCyclesTest extends TestCase
]);
}
/**
* @it no spreading itself deeply
*/
public function testNoSpreadingItselfDeeply()
{
$this->expectFailsRule(new NoFragmentCycles, '
@ -136,30 +181,36 @@ class NoFragmentCyclesTest extends TestCase
fragment fragX on Dog { ...fragY }
fragment fragY on Dog { ...fragZ }
fragment fragZ on Dog { ...fragO }
fragment fragO on Dog { ...fragA, ...fragX }
fragment fragO on Dog { ...fragP }
fragment fragP on Dog { ...fragA, ...fragX }
', [
FormattedError::create(
NoFragmentCycles::cycleErrorMessage('fragA', ['fragB', 'fragC', 'fragO']),
NoFragmentCycles::cycleErrorMessage('fragA', [ 'fragB', 'fragC', 'fragO', 'fragP' ]),
[
new SourceLocation(2, 31),
new SourceLocation(3, 31),
new SourceLocation(4, 31),
new SourceLocation(8, 31),
new SourceLocation(9, 31),
]
),
FormattedError::create(
NoFragmentCycles::cycleErrorMessage('fragX', ['fragY', 'fragZ', 'fragO']),
NoFragmentCycles::cycleErrorMessage('fragO', [ 'fragP', 'fragX', 'fragY', 'fragZ' ]),
[
new SourceLocation(8, 31),
new SourceLocation(9, 41),
new SourceLocation(5, 31),
new SourceLocation(6, 31),
new SourceLocation(7, 31),
new SourceLocation(8, 41),
]
)
]);
}
public function testNoSpreadingItselfDeeplyTwoPathsNewRule()
/**
* @it no spreading itself deeply two paths
*/
public function testNoSpreadingItselfDeeplyTwoPaths()
{
$this->expectFailsRule(new NoFragmentCycles, '
fragment fragA on Dog { ...fragB, ...fragC }
@ -177,6 +228,56 @@ class NoFragmentCyclesTest extends TestCase
]);
}
/**
* @it no spreading itself deeply two paths -- alt traverse order
*/
public function testNoSpreadingItselfDeeplyTwoPathsTraverseOrder()
{
$this->expectFailsRule(new NoFragmentCycles, '
fragment fragA on Dog { ...fragC }
fragment fragB on Dog { ...fragC }
fragment fragC on Dog { ...fragA, ...fragB }
', [
FormattedError::create(
NoFragmentCycles::cycleErrorMessage('fragA', [ 'fragC' ]),
[new SourceLocation(2,31), new SourceLocation(4,31)]
),
FormattedError::create(
NoFragmentCycles::cycleErrorMessage('fragC', [ 'fragB' ]),
[new SourceLocation(4, 41), new SourceLocation(3, 31)]
)
]);
}
/**
* @it no spreading itself deeply and immediately
*/
public function testNoSpreadingItselfDeeplyAndImmediately()
{
$this->expectFailsRule(new NoFragmentCycles, '
fragment fragA on Dog { ...fragB }
fragment fragB on Dog { ...fragB, ...fragC }
fragment fragC on Dog { ...fragA, ...fragB }
', [
FormattedError::create(
NoFragmentCycles::cycleErrorMessage('fragB', []),
[new SourceLocation(3, 31)]
),
FormattedError::create(
NoFragmentCycles::cycleErrorMessage('fragA', [ 'fragB', 'fragC' ]),
[
new SourceLocation(2, 31),
new SourceLocation(3, 41),
new SourceLocation(4, 31)
]
),
FormattedError::create(
NoFragmentCycles::cycleErrorMessage('fragB', [ 'fragC' ]),
[new SourceLocation(3, 41), new SourceLocation(4, 41)]
)
]);
}
private function cycleError($fargment, $spreadNames, $line, $column)
{
return FormattedError::create(

View File

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

View File

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

View File

@ -3,6 +3,7 @@ namespace GraphQL\Tests\Validator;
use GraphQL\Language\Parser;
use GraphQL\Schema;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
@ -43,12 +44,24 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
],
]);
$Canine = new InterfaceType([
'name' => 'Canine',
'fields' => function() {
return [
'name' => [
'type' => Type::string(),
'args' => ['surname' => ['type' => Type::boolean()]]
]
];
}
]);
$DogCommand = new EnumType([
'name' => 'DogCommand',
'values' => [
'SIT' => ['value' => 0],
'HEEL' => ['value' => 1],
'DOWN' => ['value' => 3]
'DOWN' => ['value' => 2]
]
]);
@ -76,7 +89,7 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
'args' => ['x' => ['type' => Type::int()], 'y' => ['type' => Type::int()]]
]
],
'interfaces' => [$Being, $Pet]
'interfaces' => [$Being, $Pet, $Canine]
]);
$Cat = new ObjectType([
@ -277,7 +290,15 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
]
]);
$defaultSchema = new Schema($queryRoot);
$defaultSchema = new Schema([
'query' => $queryRoot,
'directives' => [
new Directive([
'name' => 'operationOnly',
'locations' => [ 'QUERY' ],
])
]
]);
return $defaultSchema;
}