mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-22 04:46:04 +03:00
Continue updating validator rules for april2016 spec
This commit is contained in:
parent
8ab7a9a438
commit
800d8ba25f
@ -674,10 +674,9 @@ class Parser
|
||||
{
|
||||
$start = $this->token->start;
|
||||
$this->expect(Token::BRACE_L);
|
||||
$fieldNames = [];
|
||||
$fields = [];
|
||||
while (!$this->skip(Token::BRACE_R)) {
|
||||
$fields[] = $this->parseObjectField($isConst, $fieldNames);
|
||||
$fields[] = $this->parseObjectField($isConst);
|
||||
}
|
||||
return new ObjectValue([
|
||||
'fields' => $fields,
|
||||
@ -685,15 +684,11 @@ class Parser
|
||||
]);
|
||||
}
|
||||
|
||||
function parseObjectField($isConst, &$fieldNames)
|
||||
function parseObjectField($isConst)
|
||||
{
|
||||
$start = $this->token->start;
|
||||
$name = $this->parseName();
|
||||
|
||||
if (array_key_exists($name->value, $fieldNames)) {
|
||||
throw new SyntaxError($this->source, $start, "Duplicate input object field " . $name->value . '.');
|
||||
}
|
||||
$fieldNames[$name->value] = true;
|
||||
$this->expect(Token::COLON);
|
||||
|
||||
return new ObjectField([
|
||||
|
@ -30,7 +30,7 @@ class NonNull extends Type implements WrappingType, OutputType, InputType
|
||||
|
||||
/**
|
||||
* @param bool $recurse
|
||||
* @return Type
|
||||
* @return mixed
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getWrappedType($recurse = false)
|
||||
|
@ -40,6 +40,11 @@ use GraphQL\Validator\Rules\ProvidedNonNullArguments;
|
||||
use GraphQL\Validator\Rules\QueryComplexity;
|
||||
use GraphQL\Validator\Rules\QueryDepth;
|
||||
use GraphQL\Validator\Rules\ScalarLeafs;
|
||||
use GraphQL\Validator\Rules\UniqueArgumentNames;
|
||||
use GraphQL\Validator\Rules\UniqueFragmentNames;
|
||||
use GraphQL\Validator\Rules\UniqueInputFieldNames;
|
||||
use GraphQL\Validator\Rules\UniqueOperationNames;
|
||||
use GraphQL\Validator\Rules\UniqueVariableNames;
|
||||
use GraphQL\Validator\Rules\VariablesAreInputTypes;
|
||||
use GraphQL\Validator\Rules\VariablesInAllowedPosition;
|
||||
|
||||
@ -65,30 +70,30 @@ class DocumentValidator
|
||||
{
|
||||
if (null === self::$defaultRules) {
|
||||
self::$defaultRules = [
|
||||
// 'UniqueOperationNames' => new UniqueOperationNames(),
|
||||
'UniqueOperationNames' => new UniqueOperationNames(),
|
||||
'LoneAnonymousOperation' => new LoneAnonymousOperation(),
|
||||
'KnownTypeNames' => new KnownTypeNames(),
|
||||
'FragmentsOnCompositeTypes' => new FragmentsOnCompositeTypes(),
|
||||
'VariablesAreInputTypes' => new VariablesAreInputTypes(),
|
||||
'ScalarLeafs' => new ScalarLeafs(),
|
||||
'FieldsOnCorrectType' => new FieldsOnCorrectType(),
|
||||
// 'UniqueFragmentNames' => new UniqueFragmentNames(),
|
||||
'UniqueFragmentNames' => new UniqueFragmentNames(),
|
||||
'KnownFragmentNames' => new KnownFragmentNames(),
|
||||
'NoUnusedFragments' => new NoUnusedFragments(),
|
||||
'PossibleFragmentSpreads' => new PossibleFragmentSpreads(),
|
||||
'NoFragmentCycles' => new NoFragmentCycles(),
|
||||
// 'UniqueVariableNames' => new UniqueVariableNames(),
|
||||
'UniqueVariableNames' => new UniqueVariableNames(),
|
||||
'NoUndefinedVariables' => new NoUndefinedVariables(),
|
||||
'NoUnusedVariables' => new NoUnusedVariables(),
|
||||
'KnownDirectives' => new KnownDirectives(),
|
||||
'KnownArgumentNames' => new KnownArgumentNames(),
|
||||
// 'UniqueArgumentNames' => new UniqueArgumentNames(),
|
||||
'UniqueArgumentNames' => new UniqueArgumentNames(),
|
||||
'ArgumentsOfCorrectType' => new ArgumentsOfCorrectType(),
|
||||
'ProvidedNonNullArguments' => new ProvidedNonNullArguments(),
|
||||
'DefaultValuesOfCorrectType' => new DefaultValuesOfCorrectType(),
|
||||
'VariablesInAllowedPosition' => new VariablesInAllowedPosition(),
|
||||
'OverlappingFieldsCanBeMerged' => new OverlappingFieldsCanBeMerged(),
|
||||
// 'UniqueInputFieldNames' => new UniqueInputFieldNames(),
|
||||
'UniqueInputFieldNames' => new UniqueInputFieldNames(),
|
||||
|
||||
// Query Security
|
||||
'QueryDepth' => new QueryDepth(QueryDepth::DISABLED), // default disabled
|
||||
@ -259,141 +264,5 @@ class DocumentValidator
|
||||
}
|
||||
Visitor::visit($documentAST, Visitor::visitWithTypeInfo($typeInfo, Visitor::visitInParallel($visitors)));
|
||||
return $context->getErrors();
|
||||
|
||||
|
||||
|
||||
$errors = [];
|
||||
|
||||
// TODO: convert to class
|
||||
$visitInstances = function($ast, $instances) use ($typeInfo, $context, &$errors, &$visitInstances) {
|
||||
$skipUntil = new \SplFixedArray(count($instances));
|
||||
$skipCount = 0;
|
||||
|
||||
Visitor::visit($ast, [
|
||||
'enter' => function ($node, $key) use ($typeInfo, $instances, $skipUntil, &$skipCount, &$errors, $context, $visitInstances) {
|
||||
$typeInfo->enter($node);
|
||||
for ($i = 0; $i < count($instances); $i++) {
|
||||
// Do not visit this instance if it returned false for a previous node
|
||||
if ($skipUntil[$i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = null;
|
||||
|
||||
// Do not visit top level fragment definitions if this instance will
|
||||
// visit those fragments inline because it
|
||||
// provided `visitSpreadFragments`.
|
||||
if ($node->kind === Node::FRAGMENT_DEFINITION && $key !== null && !empty($instances[$i]['visitSpreadFragments'])) {
|
||||
$result = Visitor::skipNode();
|
||||
} else {
|
||||
$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());
|
||||
} else {
|
||||
$result = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($result instanceof VisitorOperation) {
|
||||
if ($result->doContinue) {
|
||||
$skipUntil[$i] = $node;
|
||||
$skipCount++;
|
||||
// If all instances are being skipped over, skip deeper traversal
|
||||
if ($skipCount === count($instances)) {
|
||||
for ($k = 0; $k < count($instances); $k++) {
|
||||
if ($skipUntil[$k] === $node) {
|
||||
$skipUntil[$k] = null;
|
||||
$skipCount--;
|
||||
}
|
||||
}
|
||||
return Visitor::skipNode();
|
||||
}
|
||||
} else if ($result->doBreak) {
|
||||
$instances[$i] = null;
|
||||
}
|
||||
} else if ($result && static::isError($result)) {
|
||||
static::append($errors, $result);
|
||||
for ($j = $i - 1; $j >= 0; $j--) {
|
||||
$leaveFn = Visitor::getVisitFn($instances[$j], $node->kind, true);
|
||||
if ($leaveFn) {
|
||||
// $leaveFn = $leaveFn->bindTo($instances[$j])
|
||||
$result = call_user_func_array($leaveFn, func_get_args());
|
||||
|
||||
if ($result instanceof VisitorOperation) {
|
||||
if ($result->doBreak) {
|
||||
$instances[$j] = null;
|
||||
}
|
||||
} else if (static::isError($result)) {
|
||||
static::append($errors, $result);
|
||||
} else if ($result !== null) {
|
||||
throw new \Exception("Config cannot edit document.");
|
||||
}
|
||||
}
|
||||
}
|
||||
$typeInfo->leave($node);
|
||||
return Visitor::skipNode();
|
||||
} else if ($result !== null) {
|
||||
throw new \Exception("Config cannot edit document.");
|
||||
}
|
||||
}
|
||||
|
||||
// If any validation instances provide the flag `visitSpreadFragments`
|
||||
// and this node is a fragment spread, validate the fragment from
|
||||
// this point.
|
||||
if ($node instanceof FragmentSpread) {
|
||||
$fragment = $context->getFragment($node->name->value);
|
||||
if ($fragment) {
|
||||
$fragVisitingInstances = [];
|
||||
foreach ($instances as $idx => $inst) {
|
||||
if (!empty($inst['visitSpreadFragments']) && !$skipUntil[$idx]) {
|
||||
$fragVisitingInstances[] = $inst;
|
||||
}
|
||||
}
|
||||
if (!empty($fragVisitingInstances)) {
|
||||
$visitInstances($fragment, $fragVisitingInstances);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'leave' => function ($node) use ($instances, $typeInfo, $skipUntil, &$skipCount, &$errors) {
|
||||
for ($i = count($instances) - 1; $i >= 0; $i--) {
|
||||
if ($skipUntil[$i]) {
|
||||
if ($skipUntil[$i] === $node) {
|
||||
$skipUntil[$i] = null;
|
||||
$skipCount--;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$leaveFn = Visitor::getVisitFn($instances[$i], $node->kind, true);
|
||||
|
||||
if ($leaveFn) {
|
||||
// $leaveFn = $leaveFn.bindTo($instances[$i]);
|
||||
$result = call_user_func_array($leaveFn, func_get_args());
|
||||
|
||||
if ($result instanceof VisitorOperation) {
|
||||
if ($result->doBreak) {
|
||||
$instances[$i] = null;
|
||||
}
|
||||
} else if (static::isError($result)) {
|
||||
static::append($errors, $result);
|
||||
} else if ($result !== null) {
|
||||
throw new \Exception("Config cannot edit document.");
|
||||
}
|
||||
}
|
||||
}
|
||||
$typeInfo->leave($node);
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
// Visit the whole document with instances of all provided rules.
|
||||
$allRuleInstances = [];
|
||||
foreach ($rules as $rule) {
|
||||
$allRuleInstances[] = call_user_func_array($rule, [$context]);
|
||||
}
|
||||
$visitInstances($documentAST, $allRuleInstances);
|
||||
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
|
@ -114,12 +114,6 @@ class Messages
|
||||
"got: $value.";
|
||||
}
|
||||
|
||||
static function badVarPosMessage($varName, $varType, $expectedType)
|
||||
{
|
||||
return "Variable \$$varName of type $varType used in position expecting ".
|
||||
"type $expectedType.";
|
||||
}
|
||||
|
||||
static function fieldsConflictMessage($responseName, $reason)
|
||||
{
|
||||
$reasonMessage = self::reasonMessage($reason);
|
||||
|
@ -4,58 +4,55 @@ namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\OperationDefinition;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\Messages;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class NoUnusedVariables
|
||||
{
|
||||
static function unusedVariableMessage($varName)
|
||||
static function unusedVariableMessage($varName, $opName = null)
|
||||
{
|
||||
return "Variable \"$$varName\" is never used.";
|
||||
return $opName
|
||||
? "Variable \"$$varName\" is never used in operation \"$opName\"."
|
||||
: "Variable \"$$varName\" is never used.";
|
||||
}
|
||||
|
||||
public $variableDefs;
|
||||
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
$visitedFragmentNames = new \stdClass();
|
||||
$variableDefs = [];
|
||||
$variableNameUsed = new \stdClass();
|
||||
$this->variableDefs = [];
|
||||
|
||||
return [
|
||||
// Visit FragmentDefinition after visiting FragmentSpread
|
||||
'visitSpreadFragments' => true,
|
||||
Node::OPERATION_DEFINITION => [
|
||||
'enter' => function() use (&$visitedFragmentNames, &$variableDefs, &$variableNameUsed) {
|
||||
$visitedFragmentNames = new \stdClass();
|
||||
$variableDefs = [];
|
||||
$variableNameUsed = new \stdClass();
|
||||
'enter' => function() {
|
||||
$this->variableDefs = [];
|
||||
},
|
||||
'leave' => function() use (&$visitedFragmentNames, &$variableDefs, &$variableNameUsed) {
|
||||
$errors = [];
|
||||
foreach ($variableDefs as $def) {
|
||||
if (empty($variableNameUsed->{$def->variable->name->value})) {
|
||||
$errors[] = new Error(
|
||||
self::unusedVariableMessage($def->variable->name->value),
|
||||
[$def]
|
||||
);
|
||||
'leave' => function(OperationDefinition $operation) use ($context) {
|
||||
$variableNameUsed = [];
|
||||
$usages = $context->getRecursiveVariableUsages($operation);
|
||||
$opName = $operation->name ? $operation->name->value : null;
|
||||
|
||||
foreach ($usages as $usage) {
|
||||
$node = $usage['node'];
|
||||
$variableNameUsed[$node->name->value] = true;
|
||||
}
|
||||
|
||||
foreach ($this->variableDefs as $variableDef) {
|
||||
$variableName = $variableDef->variable->name->value;
|
||||
|
||||
if (empty($variableNameUsed[$variableName])) {
|
||||
$context->reportError(new Error(
|
||||
self::unusedVariableMessage($variableName, $opName),
|
||||
[$variableDef]
|
||||
));
|
||||
}
|
||||
}
|
||||
return !empty($errors) ? $errors : null;
|
||||
}
|
||||
],
|
||||
Node::VARIABLE_DEFINITION => function($def) use (&$variableDefs) {
|
||||
$variableDefs[] = $def;
|
||||
return Visitor::skipNode();
|
||||
},
|
||||
Node::VARIABLE => function($variable) use (&$variableNameUsed) {
|
||||
$variableNameUsed->{$variable->name->value} = true;
|
||||
},
|
||||
Node::FRAGMENT_SPREAD => function($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($def) {
|
||||
$this->variableDefs[] = $def;
|
||||
}
|
||||
];
|
||||
}
|
||||
|
@ -4,12 +4,17 @@ namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\Directive;
|
||||
use GraphQL\Language\AST\Field;
|
||||
use GraphQL\Language\AST\FragmentSpread;
|
||||
use GraphQL\Language\AST\InlineFragment;
|
||||
use GraphQL\Language\AST\NamedType;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\SelectionSet;
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\OutputType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Utils;
|
||||
use GraphQL\Utils\PairSet;
|
||||
@ -37,31 +42,37 @@ class OverlappingFieldsCanBeMerged
|
||||
return $reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var PairSet
|
||||
*/
|
||||
public $comparedSet;
|
||||
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
$comparedSet = new PairSet();
|
||||
$this->comparedSet = new PairSet();
|
||||
|
||||
return [
|
||||
Node::SELECTION_SET => [
|
||||
// Note: we validate on the reverse traversal so deeper conflicts will be
|
||||
// caught first, for clearer error messages.
|
||||
'leave' => function(SelectionSet $selectionSet) use ($context, $comparedSet) {
|
||||
'leave' => function(SelectionSet $selectionSet) use ($context) {
|
||||
$fieldMap = $this->collectFieldASTsAndDefs(
|
||||
$context,
|
||||
$context->getParentType(),
|
||||
$selectionSet
|
||||
);
|
||||
|
||||
$conflicts = $this->findConflicts($fieldMap, $context, $comparedSet);
|
||||
$conflicts = $this->findConflicts(false, $fieldMap, $context);
|
||||
|
||||
foreach ($conflicts as $conflict) {
|
||||
$responseName = $conflict[0][0];
|
||||
$reason = $conflict[0][1];
|
||||
$fields = $conflict[1];
|
||||
$fields1 = $conflict[1];
|
||||
$fields2 = $conflict[2];
|
||||
|
||||
$context->reportError(new Error(
|
||||
self::fieldsConflictMessage($responseName, $reason),
|
||||
$fields
|
||||
array_merge($fields1, $fields2)
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -69,7 +80,7 @@ class OverlappingFieldsCanBeMerged
|
||||
];
|
||||
}
|
||||
|
||||
private function findConflicts($fieldMap, ValidationContext $context, PairSet $comparedSet)
|
||||
private function findConflicts($parentFieldsAreMutuallyExclusive, $fieldMap, ValidationContext $context)
|
||||
{
|
||||
$conflicts = [];
|
||||
foreach ($fieldMap as $responseName => $fields) {
|
||||
@ -77,7 +88,14 @@ class OverlappingFieldsCanBeMerged
|
||||
if ($count > 1) {
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
for ($j = $i; $j < $count; $j++) {
|
||||
$conflict = $this->findConflict($responseName, $fields[$i], $fields[$j], $context, $comparedSet);
|
||||
$conflict = $this->findConflict(
|
||||
$parentFieldsAreMutuallyExclusive,
|
||||
$responseName,
|
||||
$fields[$i],
|
||||
$fields[$j],
|
||||
$context
|
||||
);
|
||||
|
||||
if ($conflict) {
|
||||
$conflicts[] = $conflict;
|
||||
}
|
||||
@ -89,40 +107,70 @@ class OverlappingFieldsCanBeMerged
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ValidationContext $context
|
||||
* @param PairSet $comparedSet
|
||||
* @param $parentFieldsAreMutuallyExclusive
|
||||
* @param $responseName
|
||||
* @param [Field, GraphQLFieldDefinition] $pair1
|
||||
* @param [Field, GraphQLFieldDefinition] $pair2
|
||||
* @param ValidationContext $context
|
||||
* @return array|null
|
||||
*/
|
||||
private function findConflict($responseName, array $pair1, array $pair2, ValidationContext $context, PairSet $comparedSet)
|
||||
private function findConflict(
|
||||
$parentFieldsAreMutuallyExclusive,
|
||||
$responseName,
|
||||
array $pair1,
|
||||
array $pair2,
|
||||
ValidationContext $context
|
||||
)
|
||||
{
|
||||
list($ast1, $def1) = $pair1;
|
||||
list($ast2, $def2) = $pair2;
|
||||
list($parentType1, $ast1, $def1) = $pair1;
|
||||
list($parentType2, $ast2, $def2) = $pair2;
|
||||
|
||||
if ($ast1 === $ast2 || $comparedSet->has($ast1, $ast2)) {
|
||||
// Not a pair.
|
||||
if ($ast1 === $ast2) {
|
||||
return null;
|
||||
}
|
||||
$comparedSet->add($ast1, $ast2);
|
||||
|
||||
// Memoize, do not report the same issue twice.
|
||||
// Note: Two overlapping ASTs could be encountered both when
|
||||
// `parentFieldsAreMutuallyExclusive` is true and is false, which could
|
||||
// produce different results (when `true` being a subset of `false`).
|
||||
// However we do not need to include this piece of information when
|
||||
// memoizing since this rule visits leaf fields before their parent fields,
|
||||
// ensuring that `parentFieldsAreMutuallyExclusive` is `false` the first
|
||||
// time two overlapping fields are encountered, ensuring that the full
|
||||
// set of validation rules are always checked when necessary.
|
||||
if ($this->comparedSet->has($ast1, $ast2)) {
|
||||
return null;
|
||||
}
|
||||
$this->comparedSet->add($ast1, $ast2);
|
||||
|
||||
// The return type for each field.
|
||||
$type1 = isset($def1) ? $def1->getType() : null;
|
||||
$type2 = isset($def2) ? $def2->getType() : null;
|
||||
|
||||
// If it is known that two fields could not possibly apply at the same
|
||||
// time, due to the parent types, then it is safe to permit them to diverge
|
||||
// in aliased field or arguments used as they will not present any ambiguity
|
||||
// by differing.
|
||||
// It is known that two parent types could never overlap if they are
|
||||
// different Object types. Interface or Union types might overlap - if not
|
||||
// in the current state of the schema, then perhaps in some future version,
|
||||
// thus may not safely diverge.
|
||||
$fieldsAreMutuallyExclusive =
|
||||
$parentFieldsAreMutuallyExclusive ||
|
||||
$parentType1 !== $parentType2 &&
|
||||
$parentType1 instanceof ObjectType &&
|
||||
$parentType2 instanceof ObjectType;
|
||||
|
||||
if (!$fieldsAreMutuallyExclusive) {
|
||||
$name1 = $ast1->name->value;
|
||||
$name2 = $ast2->name->value;
|
||||
|
||||
if ($name1 !== $name2) {
|
||||
return [
|
||||
[$responseName, "$name1 and $name2 are different fields"],
|
||||
[$ast1, $ast2]
|
||||
];
|
||||
}
|
||||
|
||||
$type1 = isset($def1) ? $def1->getType() : null;
|
||||
$type2 = isset($def2) ? $def2->getType() : null;
|
||||
|
||||
if ($type1 && $type2 && !$this->sameType($type1, $type2)) {
|
||||
return [
|
||||
[$responseName, "they return differing types $type1 and $type2"],
|
||||
[$ast1, $ast2]
|
||||
[$ast1],
|
||||
[$ast2]
|
||||
];
|
||||
}
|
||||
|
||||
@ -132,26 +180,41 @@ class OverlappingFieldsCanBeMerged
|
||||
if (!$this->sameArguments($args1, $args2)) {
|
||||
return [
|
||||
[$responseName, 'they have differing arguments'],
|
||||
[$ast1, $ast2]
|
||||
[$ast1],
|
||||
[$ast2]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$directives1 = isset($ast1->directives) ? $ast1->directives : [];
|
||||
$directives2 = isset($ast2->directives) ? $ast2->directives : [];
|
||||
|
||||
if (!$this->sameDirectives($directives1, $directives2)) {
|
||||
if ($type1 && $type2 && $this->doTypesConflict($type1, $type2)) {
|
||||
return [
|
||||
[$responseName, 'they have differing directives'],
|
||||
[$ast1, $ast2]
|
||||
[$responseName, "they return conflicting types $type1 and $type2"],
|
||||
[$ast1],
|
||||
[$ast2]
|
||||
];
|
||||
}
|
||||
|
||||
$selectionSet1 = isset($ast1->selectionSet) ? $ast1->selectionSet : null;
|
||||
$selectionSet2 = isset($ast2->selectionSet) ? $ast2->selectionSet : null;
|
||||
$subfieldMap = $this->getSubfieldMap($ast1, $type1, $ast2, $type2, $context);
|
||||
|
||||
if ($subfieldMap) {
|
||||
$conflicts = $this->findConflicts($fieldsAreMutuallyExclusive, $subfieldMap, $context);
|
||||
return $this->subfieldConflicts($conflicts, $responseName, $ast1, $ast2);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getSubfieldMap(
|
||||
Field $ast1,
|
||||
$type1,
|
||||
Field $ast2,
|
||||
$type2,
|
||||
ValidationContext $context
|
||||
) {
|
||||
$selectionSet1 = $ast1->selectionSet;
|
||||
$selectionSet2 = $ast2->selectionSet;
|
||||
if ($selectionSet1 && $selectionSet2) {
|
||||
$visitedFragmentNames = new \ArrayObject();
|
||||
|
||||
$subfieldMap = $this->collectFieldASTsAndDefs(
|
||||
$context,
|
||||
Type::getNamedType($type1),
|
||||
@ -165,15 +228,68 @@ class OverlappingFieldsCanBeMerged
|
||||
$visitedFragmentNames,
|
||||
$subfieldMap
|
||||
);
|
||||
$conflicts = $this->findConflicts($subfieldMap, $context, $comparedSet);
|
||||
return $subfieldMap;
|
||||
}
|
||||
}
|
||||
|
||||
private function subfieldConflicts(
|
||||
array $conflicts,
|
||||
$responseName,
|
||||
Field $ast1,
|
||||
Field $ast2
|
||||
)
|
||||
{
|
||||
if (!empty($conflicts)) {
|
||||
return [
|
||||
[$responseName, array_map(function ($conflict) { return $conflict[0]; }, $conflicts)],
|
||||
array_reduce($conflicts, function ($allFields, $conflict) { return array_merge($allFields, $conflict[1]); }, [$ast1, $ast2])
|
||||
[
|
||||
$responseName,
|
||||
Utils::map($conflicts, function($conflict) {return $conflict[0];})
|
||||
],
|
||||
array_reduce(
|
||||
$conflicts,
|
||||
function($allFields, $conflict) { return array_merge($allFields, $conflict[1]);},
|
||||
[ $ast1 ]
|
||||
),
|
||||
array_reduce(
|
||||
$conflicts,
|
||||
function($allFields, $conflict) {return array_merge($allFields, $conflict[2]);},
|
||||
[ $ast2 ]
|
||||
)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputType $type1
|
||||
* @param OutputType $type2
|
||||
* @return bool
|
||||
*/
|
||||
private function doTypesConflict(OutputType $type1, OutputType $type2)
|
||||
{
|
||||
if ($type1 instanceof ListOfType) {
|
||||
return $type2 instanceof ListOfType ?
|
||||
$this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) :
|
||||
true;
|
||||
}
|
||||
if ($type2 instanceof ListOfType) {
|
||||
return $type1 instanceof ListOfType ?
|
||||
$this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) :
|
||||
true;
|
||||
}
|
||||
if ($type1 instanceof NonNull) {
|
||||
return $type2 instanceof NonNull ?
|
||||
$this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) :
|
||||
true;
|
||||
}
|
||||
if ($type2 instanceof NonNull) {
|
||||
return $type1 instanceof NonNull ?
|
||||
$this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) :
|
||||
true;
|
||||
}
|
||||
if (Type::isLeafType($type1) || Type::isLeafType($type2)) {
|
||||
return $type1 !== $type2;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -185,7 +301,7 @@ class OverlappingFieldsCanBeMerged
|
||||
* spread in all fragments.
|
||||
*
|
||||
* @param ValidationContext $context
|
||||
* @param Type|null $parentType
|
||||
* @param mixed $parentType
|
||||
* @param SelectionSet $selectionSet
|
||||
* @param \ArrayObject $visitedFragmentNames
|
||||
* @param \ArrayObject $astAndDefs
|
||||
@ -214,13 +330,17 @@ class OverlappingFieldsCanBeMerged
|
||||
if (!isset($_astAndDefs[$responseName])) {
|
||||
$_astAndDefs[$responseName] = new \ArrayObject();
|
||||
}
|
||||
$_astAndDefs[$responseName][] = [$selection, $fieldDef];
|
||||
$_astAndDefs[$responseName][] = [$parentType, $selection, $fieldDef];
|
||||
break;
|
||||
case Node::INLINE_FRAGMENT:
|
||||
/** @var InlineFragment $inlineFragment */
|
||||
$typeCondition = $selection->typeCondition;
|
||||
$inlineFragmentType = $typeCondition
|
||||
? TypeInfo::typeFromAST($context->getSchema(), $typeCondition)
|
||||
: $parentType;
|
||||
|
||||
$_astAndDefs = $this->collectFieldASTsAndDefs(
|
||||
$context,
|
||||
TypeInfo::typeFromAST($context->getSchema(), $selection->typeCondition),
|
||||
$inlineFragmentType,
|
||||
$selection->selectionSet,
|
||||
$_visitedFragmentNames,
|
||||
$_astAndDefs
|
||||
@ -237,9 +357,10 @@ class OverlappingFieldsCanBeMerged
|
||||
if (!$fragment) {
|
||||
continue;
|
||||
}
|
||||
$fragmentType = TypeInfo::typeFromAST($context->getSchema(), $fragment->typeCondition);
|
||||
$_astAndDefs = $this->collectFieldASTsAndDefs(
|
||||
$context,
|
||||
TypeInfo::typeFromAST($context->getSchema(), $fragment->typeCondition),
|
||||
$fragmentType,
|
||||
$fragment->selectionSet,
|
||||
$_visitedFragmentNames,
|
||||
$_astAndDefs
|
||||
@ -250,31 +371,6 @@ class OverlappingFieldsCanBeMerged
|
||||
return $_astAndDefs;
|
||||
}
|
||||
|
||||
private function sameDirectives(array $directives1, array $directives2)
|
||||
{
|
||||
if (count($directives1) !== count($directives2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($directives1 as $directive1) {
|
||||
$directive2 = null;
|
||||
foreach ($directives2 as $tmp) {
|
||||
if ($tmp->name->value === $directive1->name->value) {
|
||||
$directive2 = $tmp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$directive2) {
|
||||
return false;
|
||||
}
|
||||
if (!$this->sameArguments($directive1->arguments, $directive2->arguments)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param Array<Argument | Directive> $pairs1
|
||||
* @param Array<Argument | Directive> $pairs2
|
||||
|
@ -6,12 +6,9 @@ use GraphQL\Error;
|
||||
use GraphQL\Language\AST\FragmentSpread;
|
||||
use GraphQL\Language\AST\InlineFragment;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Utils;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
|
||||
class PossibleFragmentSpreads
|
||||
{
|
||||
@ -29,9 +26,10 @@ class PossibleFragmentSpreads
|
||||
{
|
||||
return [
|
||||
Node::INLINE_FRAGMENT => function(InlineFragment $node) use ($context) {
|
||||
$fragType = Type::getNamedType($context->getType());
|
||||
$fragType = $context->getType();
|
||||
$parentType = $context->getParentType();
|
||||
if ($fragType && $parentType && !$this->doTypesOverlap($fragType, $parentType)) {
|
||||
|
||||
if ($fragType && $parentType && !TypeInfo::doTypesOverlap($context->getSchema(), $fragType, $parentType)) {
|
||||
$context->reportError(new Error(
|
||||
self::typeIncompatibleAnonSpreadMessage($parentType, $fragType),
|
||||
[$node]
|
||||
@ -40,10 +38,10 @@ class PossibleFragmentSpreads
|
||||
},
|
||||
Node::FRAGMENT_SPREAD => function(FragmentSpread $node) use ($context) {
|
||||
$fragName = $node->name->value;
|
||||
$fragType = Type::getNamedType($this->getFragmentType($context, $fragName));
|
||||
$fragType = $this->getFragmentType($context, $fragName);
|
||||
$parentType = $context->getParentType();
|
||||
|
||||
if ($fragType && $parentType && !$this->doTypesOverlap($fragType, $parentType)) {
|
||||
if ($fragType && $parentType && !TypeInfo::doTypesOverlap($context->getSchema(), $fragType, $parentType)) {
|
||||
$context->reportError(new Error(
|
||||
self::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType),
|
||||
[$node]
|
||||
@ -56,33 +54,6 @@ class PossibleFragmentSpreads
|
||||
private function getFragmentType(ValidationContext $context, $name)
|
||||
{
|
||||
$frag = $context->getFragment($name);
|
||||
return $frag ? Utils\TypeInfo::typeFromAST($context->getSchema(), $frag->typeCondition) : null;
|
||||
}
|
||||
|
||||
private function doTypesOverlap($t1, $t2)
|
||||
{
|
||||
if ($t1 === $t2) {
|
||||
return true;
|
||||
}
|
||||
if ($t1 instanceof ObjectType) {
|
||||
if ($t2 instanceof ObjectType) {
|
||||
return false;
|
||||
}
|
||||
return in_array($t1, $t2->getPossibleTypes());
|
||||
}
|
||||
if ($t1 instanceof InterfaceType || $t1 instanceof UnionType) {
|
||||
if ($t2 instanceof ObjectType) {
|
||||
return in_array($t2, $t1->getPossibleTypes());
|
||||
}
|
||||
$t1TypeNames = Utils::keyMap($t1->getPossibleTypes(), function ($type) {
|
||||
return $type->name;
|
||||
});
|
||||
foreach ($t2->getPossibleTypes() as $type) {
|
||||
if (!empty($t1TypeNames[$type->name])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return $frag ? TypeInfo::typeFromAST($context->getSchema(), $frag->typeCondition) : null;
|
||||
}
|
||||
}
|
||||
|
@ -77,8 +77,6 @@ class QueryComplexity extends AbstractQuerySecurity
|
||||
return $this->invokeIfNeeded(
|
||||
$context,
|
||||
[
|
||||
// Visit FragmentDefinition after visiting FragmentSpread
|
||||
'visitSpreadFragments' => true,
|
||||
Node::SELECTION_SET => function (SelectionSet $selectionSet) use ($context) {
|
||||
$this->fieldAstAndDefs = $this->collectFieldASTsAndDefs(
|
||||
$context,
|
||||
@ -90,7 +88,6 @@ class QueryComplexity extends AbstractQuerySecurity
|
||||
},
|
||||
Node::VARIABLE_DEFINITION => function ($def) {
|
||||
$this->variableDefs[] = $def;
|
||||
|
||||
return Visitor::skipNode();
|
||||
},
|
||||
Node::OPERATION_DEFINITION => [
|
||||
@ -98,7 +95,9 @@ class QueryComplexity extends AbstractQuerySecurity
|
||||
$complexity = $this->fieldComplexity($operationDefinition, $complexity);
|
||||
|
||||
if ($complexity > $this->getMaxQueryComplexity()) {
|
||||
return new Error($this->maxQueryComplexityErrorMessage($this->getMaxQueryComplexity(), $complexity));
|
||||
$context->reportError(
|
||||
new Error($this->maxQueryComplexityErrorMessage($this->getMaxQueryComplexity(), $complexity))
|
||||
);
|
||||
}
|
||||
},
|
||||
],
|
||||
|
@ -54,7 +54,9 @@ class QueryDepth extends AbstractQuerySecurity
|
||||
$maxDepth = $this->fieldDepth($operationDefinition);
|
||||
|
||||
if ($maxDepth > $this->getMaxQueryDepth()) {
|
||||
return new Error($this->maxQueryDepthErrorMessage($this->getMaxQueryDepth(), $maxDepth));
|
||||
$context->reportError(
|
||||
new Error($this->maxQueryDepthErrorMessage($this->getMaxQueryDepth(), $maxDepth))
|
||||
);
|
||||
}
|
||||
},
|
||||
],
|
||||
|
43
src/Validator/Rules/UniqueArgumentNames.php
Normal file
43
src/Validator/Rules/UniqueArgumentNames.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\Argument;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class UniqueArgumentNames
|
||||
{
|
||||
static function duplicateArgMessage($argName)
|
||||
{
|
||||
return "There can be only one argument named \"$argName\".";
|
||||
}
|
||||
|
||||
public $knownArgNames;
|
||||
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
$this->knownArgNames = [];
|
||||
|
||||
return [
|
||||
Node::FIELD => function () {
|
||||
$this->knownArgNames = [];;
|
||||
},
|
||||
Node::DIRECTIVE => function () {
|
||||
$this->knownArgNames = [];
|
||||
},
|
||||
Node::ARGUMENT => function (Argument $node) use ($context) {
|
||||
$argName = $node->name->value;
|
||||
if (!empty($this->knownArgNames[$argName])) {
|
||||
$context->reportError(new Error(
|
||||
self::duplicateArgMessage($argName),
|
||||
[$this->knownArgNames[$argName], $node->name]
|
||||
));
|
||||
} else {
|
||||
$this->knownArgNames[$argName] = $node->name;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
42
src/Validator/Rules/UniqueFragmentNames.php
Normal file
42
src/Validator/Rules/UniqueFragmentNames.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\Argument;
|
||||
use GraphQL\Language\AST\FragmentDefinition;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class UniqueFragmentNames
|
||||
{
|
||||
static function duplicateFragmentNameMessage($fragName)
|
||||
{
|
||||
return "There can only be one fragment named \"$fragName\".";
|
||||
}
|
||||
|
||||
public $knownFragmentNames;
|
||||
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
$this->knownFragmentNames = [];
|
||||
|
||||
return [
|
||||
Node::OPERATION_DEFINITION => function () {
|
||||
return Visitor::skipNode();
|
||||
},
|
||||
Node::FRAGMENT_DEFINITION => function (FragmentDefinition $node) use ($context) {
|
||||
$fragmentName = $node->name->value;
|
||||
if (!empty($this->knownFragmentNames[$fragmentName])) {
|
||||
$context->reportError(new Error(
|
||||
self::duplicateFragmentNameMessage($fragmentName),
|
||||
[ $this->knownFragmentNames[$fragmentName], $node->name ]
|
||||
));
|
||||
} else {
|
||||
$this->knownFragmentNames[$fragmentName] = $node->name;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
50
src/Validator/Rules/UniqueInputFieldNames.php
Normal file
50
src/Validator/Rules/UniqueInputFieldNames.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\ObjectField;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class UniqueInputFieldNames
|
||||
{
|
||||
static function duplicateInputFieldMessage($fieldName)
|
||||
{
|
||||
return "There can be only one input field named \"$fieldName\".";
|
||||
}
|
||||
|
||||
public $knownNames;
|
||||
public $knownNameStack;
|
||||
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
$this->knownNames = [];
|
||||
$this->knownNameStack = [];
|
||||
|
||||
return [
|
||||
Node::OBJECT => [
|
||||
'enter' => function() {
|
||||
$this->knownNameStack[] = $this->knownNames;
|
||||
$this->knownNames = [];
|
||||
},
|
||||
'leave' => function() {
|
||||
$this->knownNames = array_pop($this->knownNameStack);
|
||||
}
|
||||
],
|
||||
Node::OBJECT_FIELD => function(ObjectField $node) use ($context) {
|
||||
$fieldName = $node->name->value;
|
||||
|
||||
if (!empty($this->knownNames[$fieldName])) {
|
||||
$context->reportError(new Error(
|
||||
self::duplicateInputFieldMessage($fieldName),
|
||||
[ $this->knownNames[$fieldName], $node->name ]
|
||||
));
|
||||
} else {
|
||||
$this->knownNames[$fieldName] = $node->name;
|
||||
}
|
||||
return Visitor::skipNode();
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
44
src/Validator/Rules/UniqueOperationNames.php
Normal file
44
src/Validator/Rules/UniqueOperationNames.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\OperationDefinition;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class UniqueOperationNames
|
||||
{
|
||||
static function duplicateOperationNameMessage($operationName)
|
||||
{
|
||||
return "There can only be one operation named \"$operationName\".";
|
||||
}
|
||||
|
||||
public $knownOperationNames;
|
||||
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
$this->knownOperationNames = [];
|
||||
|
||||
return [
|
||||
Node::OPERATION_DEFINITION => function(OperationDefinition $node) use ($context) {
|
||||
$operationName = $node->name;
|
||||
|
||||
if ($operationName) {
|
||||
if (!empty($this->knownOperationNames[$operationName->value])) {
|
||||
$context->reportError(new Error(
|
||||
self::duplicateOperationNameMessage($operationName->value),
|
||||
[ $this->knownOperationNames[$operationName->value], $operationName ]
|
||||
));
|
||||
} else {
|
||||
$this->knownOperationNames[$operationName->value] = $operationName;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
Node::FRAGMENT_DEFINITION => function() {
|
||||
return Visitor::skipNode();
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
39
src/Validator/Rules/UniqueVariableNames.php
Normal file
39
src/Validator/Rules/UniqueVariableNames.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\VariableDefinition;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class UniqueVariableNames
|
||||
{
|
||||
static function duplicateVariableMessage($variableName)
|
||||
{
|
||||
return "There can be only one variable named \"$variableName\".";
|
||||
}
|
||||
|
||||
public $knownVariableNames;
|
||||
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
$this->knownVariableNames = [];
|
||||
|
||||
return [
|
||||
Node::OPERATION_DEFINITION => function() {
|
||||
$this->knownVariableNames = [];
|
||||
},
|
||||
Node::VARIABLE_DEFINITION => function(VariableDefinition $node) use ($context) {
|
||||
$variableName = $node->variable->name->value;
|
||||
if (!empty($this->knownVariableNames[$variableName])) {
|
||||
$context->reportError(new Error(
|
||||
self::duplicateVariableMessage($variableName),
|
||||
[ $this->knownVariableNames[$variableName], $node->variable->name ]
|
||||
));
|
||||
} else {
|
||||
$this->knownVariableNames[$variableName] = $node->variable->name;
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ namespace GraphQL\Validator\Rules;
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\FragmentSpread;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\OperationDefinition;
|
||||
use GraphQL\Language\AST\Variable;
|
||||
use GraphQL\Language\AST\VariableDefinition;
|
||||
use GraphQL\Language\Visitor;
|
||||
@ -16,43 +17,54 @@ use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class VariablesInAllowedPosition
|
||||
{
|
||||
static function badVarPosMessage($varName, $varType, $expectedType)
|
||||
{
|
||||
return "Variable \$$varName of type $varType used in position expecting ".
|
||||
"type $expectedType.";
|
||||
}
|
||||
|
||||
public $varDefMap;
|
||||
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
$varDefMap = new \ArrayObject();
|
||||
$visitedFragmentNames = new \ArrayObject();
|
||||
$varDefMap = [];
|
||||
|
||||
return [
|
||||
// Visit FragmentDefinition after visiting FragmentSpread
|
||||
'visitSpreadFragments' => true,
|
||||
Node::OPERATION_DEFINITION => function () use (&$varDefMap, &$visitedFragmentNames) {
|
||||
$varDefMap = new \ArrayObject();
|
||||
$visitedFragmentNames = new \ArrayObject();
|
||||
Node::OPERATION_DEFINITION => [
|
||||
'enter' => function () {
|
||||
$this->varDefMap = [];
|
||||
},
|
||||
Node::VARIABLE_DEFINITION => function (VariableDefinition $varDefAST) use ($varDefMap) {
|
||||
$varDefMap[$varDefAST->variable->name->value] = $varDefAST;
|
||||
},
|
||||
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 => function (Variable $variableAST) use ($context, $varDefMap) {
|
||||
$varName = $variableAST->name->value;
|
||||
$varDef = isset($varDefMap[$varName]) ? $varDefMap[$varName] : null;
|
||||
$varType = $varDef ? TypeInfo::typeFromAST($context->getSchema(), $varDef->type) : null;
|
||||
$inputType = $context->getInputType();
|
||||
'leave' => function(OperationDefinition $operation) use ($context) {
|
||||
$usages = $context->getRecursiveVariableUsages($operation);
|
||||
|
||||
if ($varType && $inputType &&
|
||||
!$this->varTypeAllowedForType($this->effectiveType($varType, $varDef), $inputType)
|
||||
) {
|
||||
foreach ($usages as $usage) {
|
||||
$node = $usage['node'];
|
||||
$type = $usage['type'];
|
||||
$varName = $node->name->value;
|
||||
$varDef = isset($this->varDefMap[$varName]) ? $this->varDefMap[$varName] : null;
|
||||
|
||||
if ($varDef && $type) {
|
||||
// A var type is allowed if it is the same or more strict (e.g. is
|
||||
// a subtype of) than the expected type. It can be more strict if
|
||||
// the variable type is non-null when the expected type is nullable.
|
||||
// If both are list types, the variable item type can be more strict
|
||||
// than the expected item type (contravariant).
|
||||
$schema = $context->getSchema();
|
||||
$varType = TypeInfo::typeFromAST($schema, $varDef->type);
|
||||
|
||||
if ($varType && !TypeInfo::isTypeSubTypeOf($schema, $this->effectiveType($varType, $varDef), $type)) {
|
||||
$context->reportError(new Error(
|
||||
Messages::badVarPosMessage($varName, $varType, $inputType),
|
||||
[$variableAST]
|
||||
self::badVarPosMessage($varName, $varType, $type),
|
||||
[$varDef, $node]
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
Node::VARIABLE_DEFINITION => function (VariableDefinition $varDefAST) {
|
||||
$this->varDefMap[$varDefAST->variable->name->value] = $varDefAST;
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -47,8 +47,8 @@ class AbstractTest extends \PHPUnit_Framework_TestCase
|
||||
]
|
||||
]);
|
||||
|
||||
$schema = new Schema(
|
||||
new ObjectType([
|
||||
$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => [
|
||||
'pets' => [
|
||||
@ -59,7 +59,7 @@ class AbstractTest extends \PHPUnit_Framework_TestCase
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
]);
|
||||
|
||||
$query = '{
|
||||
pets {
|
||||
@ -112,7 +112,8 @@ class AbstractTest extends \PHPUnit_Framework_TestCase
|
||||
'types' => [$dogType, $catType]
|
||||
]);
|
||||
|
||||
$schema = new Schema(new ObjectType([
|
||||
$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => [
|
||||
'pets' => [
|
||||
@ -122,7 +123,8 @@ class AbstractTest extends \PHPUnit_Framework_TestCase
|
||||
}
|
||||
]
|
||||
]
|
||||
]));
|
||||
])
|
||||
]);
|
||||
|
||||
$query = '{
|
||||
pets {
|
||||
|
@ -8,6 +8,10 @@ use GraphQL\Validator\Rules\NoUnusedVariables;
|
||||
class NoUnusedVariablesTest extends TestCase
|
||||
{
|
||||
// Validate: No unused variables
|
||||
|
||||
/**
|
||||
* @it uses all variables
|
||||
*/
|
||||
public function testUsesAllVariables()
|
||||
{
|
||||
$this->expectPassesRule(new NoUnusedVariables(), '
|
||||
@ -17,6 +21,9 @@ class NoUnusedVariablesTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it uses all variables deeply
|
||||
*/
|
||||
public function testUsesAllVariablesDeeply()
|
||||
{
|
||||
$this->expectPassesRule(new NoUnusedVariables, '
|
||||
@ -30,6 +37,9 @@ class NoUnusedVariablesTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it uses all variables deeply in inline fragments
|
||||
*/
|
||||
public function testUsesAllVariablesDeeplyInInlineFragments()
|
||||
{
|
||||
$this->expectPassesRule(new NoUnusedVariables, '
|
||||
@ -47,6 +57,9 @@ class NoUnusedVariablesTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it uses all variables in fragments
|
||||
*/
|
||||
public function testUsesAllVariablesInFragments()
|
||||
{
|
||||
$this->expectPassesRule(new NoUnusedVariables, '
|
||||
@ -69,6 +82,9 @@ class NoUnusedVariablesTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it variable used by fragment in multiple operations
|
||||
*/
|
||||
public function testVariableUsedByFragmentInMultipleOperations()
|
||||
{
|
||||
$this->expectPassesRule(new NoUnusedVariables, '
|
||||
@ -87,6 +103,9 @@ class NoUnusedVariablesTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it variable used by recursive fragment
|
||||
*/
|
||||
public function testVariableUsedByRecursiveFragment()
|
||||
{
|
||||
$this->expectPassesRule(new NoUnusedVariables, '
|
||||
@ -101,17 +120,23 @@ class NoUnusedVariablesTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it variable not used
|
||||
*/
|
||||
public function testVariableNotUsed()
|
||||
{
|
||||
$this->expectFailsRule(new NoUnusedVariables, '
|
||||
query Foo($a: String, $b: String, $c: String) {
|
||||
query ($a: String, $b: String, $c: String) {
|
||||
field(a: $a, b: $b)
|
||||
}
|
||||
', [
|
||||
$this->unusedVar('c', 2, 41)
|
||||
$this->unusedVar('c', null, 2, 38)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it multiple variables not used
|
||||
*/
|
||||
public function testMultipleVariablesNotUsed()
|
||||
{
|
||||
$this->expectFailsRule(new NoUnusedVariables, '
|
||||
@ -119,11 +144,14 @@ class NoUnusedVariablesTest extends TestCase
|
||||
field(b: $b)
|
||||
}
|
||||
', [
|
||||
$this->unusedVar('a', 2, 17),
|
||||
$this->unusedVar('c', 2, 41)
|
||||
$this->unusedVar('a', 'Foo', 2, 17),
|
||||
$this->unusedVar('c', 'Foo', 2, 41)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it variable not used in fragments
|
||||
*/
|
||||
public function testVariableNotUsedInFragments()
|
||||
{
|
||||
$this->expectFailsRule(new NoUnusedVariables, '
|
||||
@ -144,10 +172,13 @@ class NoUnusedVariablesTest extends TestCase
|
||||
field
|
||||
}
|
||||
', [
|
||||
$this->unusedVar('c', 2, 41)
|
||||
$this->unusedVar('c', 'Foo', 2, 41)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it multiple variables not used
|
||||
*/
|
||||
public function testMultipleVariablesNotUsed2()
|
||||
{
|
||||
$this->expectFailsRule(new NoUnusedVariables, '
|
||||
@ -168,11 +199,14 @@ class NoUnusedVariablesTest extends TestCase
|
||||
field
|
||||
}
|
||||
', [
|
||||
$this->unusedVar('a', 2, 17),
|
||||
$this->unusedVar('c', 2, 41)
|
||||
$this->unusedVar('a', 'Foo', 2, 17),
|
||||
$this->unusedVar('c', 'Foo', 2, 41)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it variable not used by unreferenced fragment
|
||||
*/
|
||||
public function testVariableNotUsedByUnreferencedFragment()
|
||||
{
|
||||
$this->expectFailsRule(new NoUnusedVariables, '
|
||||
@ -186,10 +220,13 @@ class NoUnusedVariablesTest extends TestCase
|
||||
field(b: $b)
|
||||
}
|
||||
', [
|
||||
$this->unusedVar('b', 2, 17)
|
||||
$this->unusedVar('b', 'Foo', 2, 17)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it variable not used by fragment used by other operation
|
||||
*/
|
||||
public function testVariableNotUsedByFragmentUsedByOtherOperation()
|
||||
{
|
||||
$this->expectFailsRule(new NoUnusedVariables, '
|
||||
@ -206,15 +243,15 @@ class NoUnusedVariablesTest extends TestCase
|
||||
field(b: $b)
|
||||
}
|
||||
', [
|
||||
$this->unusedVar('b', 2, 17),
|
||||
$this->unusedVar('a', 5, 17)
|
||||
$this->unusedVar('b', 'Foo', 2, 17),
|
||||
$this->unusedVar('a', 'Bar', 5, 17)
|
||||
]);
|
||||
}
|
||||
|
||||
private function unusedVar($varName, $line, $column)
|
||||
private function unusedVar($varName, $opName, $line, $column)
|
||||
{
|
||||
return FormattedError::create(
|
||||
NoUnusedVariables::unusedVariableMessage($varName),
|
||||
NoUnusedVariables::unusedVariableMessage($varName, $opName),
|
||||
[new SourceLocation($line, $column)]
|
||||
);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ use GraphQL\FormattedError;
|
||||
use GraphQL\Language\Source;
|
||||
use GraphQL\Language\SourceLocation;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
@ -14,6 +15,9 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
{
|
||||
// Validate: Overlapping fields can be merged
|
||||
|
||||
/**
|
||||
* @it unique fields
|
||||
*/
|
||||
public function testUniqueFields()
|
||||
{
|
||||
$this->expectPassesRule(new OverlappingFieldsCanBeMerged(), '
|
||||
@ -24,6 +28,9 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it identical fields
|
||||
*/
|
||||
public function testIdenticalFields()
|
||||
{
|
||||
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
||||
@ -34,6 +41,9 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it identical fields with identical args
|
||||
*/
|
||||
public function testIdenticalFieldsWithIdenticalArgs()
|
||||
{
|
||||
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
||||
@ -44,6 +54,9 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it identical fields with identical directives
|
||||
*/
|
||||
public function testIdenticalFieldsWithIdenticalDirectives()
|
||||
{
|
||||
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
||||
@ -54,6 +67,9 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it different args with different aliases
|
||||
*/
|
||||
public function testDifferentArgsWithDifferentAliases()
|
||||
{
|
||||
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
||||
@ -64,6 +80,9 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it different directives with different aliases
|
||||
*/
|
||||
public function testDifferentDirectivesWithDifferentAliases()
|
||||
{
|
||||
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
||||
@ -74,6 +93,25 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it different skip/include directives accepted
|
||||
*/
|
||||
public function testDifferentSkipIncludeDirectivesAccepted()
|
||||
{
|
||||
// Note: Differing skip/include directives don't create an ambiguous return
|
||||
// value and are acceptable in conditions where differing runtime values
|
||||
// may have the same desired effect of including or skipping a field.
|
||||
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
||||
fragment differentDirectivesWithDifferentAliases on Dog {
|
||||
name @include(if: true)
|
||||
name @include(if: false)
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Same aliases with different field targets
|
||||
*/
|
||||
public function testSameAliasesWithDifferentFieldTargets()
|
||||
{
|
||||
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
||||
@ -89,6 +127,28 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Same aliases allowed on non-overlapping fields
|
||||
*/
|
||||
public function testSameAliasesAllowedOnNonOverlappingFields()
|
||||
{
|
||||
// This is valid since no object can be both a "Dog" and a "Cat", thus
|
||||
// these fields can never overlap.
|
||||
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
||||
fragment sameAliasesWithDifferentFieldTargets on Pet {
|
||||
... on Dog {
|
||||
name
|
||||
}
|
||||
... on Cat {
|
||||
name: nickname
|
||||
}
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Alias masking direct field access
|
||||
*/
|
||||
public function testAliasMaskingDirectFieldAccess()
|
||||
{
|
||||
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
||||
@ -104,6 +164,47 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it different args, second adds an argument
|
||||
*/
|
||||
public function testDifferentArgsSecondAddsAnArgument()
|
||||
{
|
||||
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
||||
fragment conflictingArgs on Dog {
|
||||
doesKnowCommand
|
||||
doesKnowCommand(dogCommand: HEEL)
|
||||
}
|
||||
', [
|
||||
FormattedError::create(
|
||||
OverlappingFieldsCanBeMerged::fieldsConflictMessage('doesKnowCommand', 'they have differing arguments'),
|
||||
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it different args, second missing an argument
|
||||
*/
|
||||
public function testDifferentArgsSecondMissingAnArgument()
|
||||
{
|
||||
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
||||
fragment conflictingArgs on Dog {
|
||||
doesKnowCommand(dogCommand: SIT)
|
||||
doesKnowCommand
|
||||
}
|
||||
',
|
||||
[
|
||||
FormattedError::create(
|
||||
OverlappingFieldsCanBeMerged::fieldsConflictMessage('doesKnowCommand', 'they have differing arguments'),
|
||||
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it conflicting args
|
||||
*/
|
||||
public function testConflictingArgs()
|
||||
{
|
||||
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
||||
@ -119,67 +220,28 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
public function testConflictingDirectives()
|
||||
/**
|
||||
* @it allows different args where no conflict is possible
|
||||
*/
|
||||
public function testAllowsDifferentArgsWhereNoConflictIsPossible()
|
||||
{
|
||||
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
||||
fragment conflictingDirectiveArgs on Dog {
|
||||
name @include(if: true)
|
||||
name @skip(if: true)
|
||||
// This is valid since no object can be both a "Dog" and a "Cat", thus
|
||||
// these fields can never overlap.
|
||||
$this->expectPassesRule(new OverlappingFieldsCanBeMerged, '
|
||||
fragment conflictingArgs on Pet {
|
||||
... on Dog {
|
||||
name(surname: true)
|
||||
}
|
||||
', [
|
||||
FormattedError::create(
|
||||
OverlappingFieldsCanBeMerged::fieldsConflictMessage('name', 'they have differing directives'),
|
||||
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
public function testConflictingDirectiveArgs()
|
||||
{
|
||||
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
||||
fragment conflictingDirectiveArgs on Dog {
|
||||
name @include(if: true)
|
||||
name @include(if: false)
|
||||
}
|
||||
', [
|
||||
FormattedError::create(
|
||||
OverlappingFieldsCanBeMerged::fieldsConflictMessage('name', 'they have differing directives'),
|
||||
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function testConflictingArgsWithMatchingDirectives()
|
||||
{
|
||||
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
||||
fragment conflictingArgsWithMatchingDirectiveArgs on Dog {
|
||||
doesKnowCommand(dogCommand: SIT) @include(if: true)
|
||||
doesKnowCommand(dogCommand: HEEL) @include(if: true)
|
||||
}
|
||||
', [
|
||||
FormattedError::create(
|
||||
OverlappingFieldsCanBeMerged::fieldsConflictMessage('doesKnowCommand', 'they have differing arguments'),
|
||||
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
public function testConflictingDirectivesWithMatchingArgs()
|
||||
{
|
||||
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
||||
fragment conflictingDirectiveArgsWithMatchingArgs on Dog {
|
||||
doesKnowCommand(dogCommand: SIT) @include(if: true)
|
||||
doesKnowCommand(dogCommand: SIT) @skip(if: true)
|
||||
}
|
||||
', [
|
||||
FormattedError::create(
|
||||
OverlappingFieldsCanBeMerged::fieldsConflictMessage('doesKnowCommand', 'they have differing directives'),
|
||||
[new SourceLocation(3, 9), new SourceLocation(4, 9)]
|
||||
)
|
||||
]);
|
||||
... on Cat {
|
||||
name
|
||||
}
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it encounters conflict in fragments
|
||||
*/
|
||||
public function testEncountersConflictInFragments()
|
||||
{
|
||||
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
||||
@ -201,6 +263,9 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it reports each conflict once
|
||||
*/
|
||||
public function testReportsEachConflictOnce()
|
||||
{
|
||||
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
||||
@ -241,6 +306,9 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it deep conflict
|
||||
*/
|
||||
public function testDeepConflict()
|
||||
{
|
||||
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
||||
@ -257,14 +325,17 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
OverlappingFieldsCanBeMerged::fieldsConflictMessage('field', [['x', 'a and b are different fields']]),
|
||||
[
|
||||
new SourceLocation(3, 9),
|
||||
new SourceLocation(6,9),
|
||||
new SourceLocation(4, 11),
|
||||
new SourceLocation(6,9),
|
||||
new SourceLocation(7, 11)
|
||||
]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it deep conflict with multiple issues
|
||||
*/
|
||||
public function testDeepConflictWithMultipleIssues()
|
||||
{
|
||||
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
||||
@ -286,16 +357,19 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
]),
|
||||
[
|
||||
new SourceLocation(3,9),
|
||||
new SourceLocation(7,9),
|
||||
new SourceLocation(4,11),
|
||||
new SourceLocation(8,11),
|
||||
new SourceLocation(5,11),
|
||||
new SourceLocation(7,9),
|
||||
new SourceLocation(8,11),
|
||||
new SourceLocation(9,11)
|
||||
]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it very deep conflict
|
||||
*/
|
||||
public function testVeryDeepConflict()
|
||||
{
|
||||
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
||||
@ -316,16 +390,19 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
OverlappingFieldsCanBeMerged::fieldsConflictMessage('field', [['deepField', [['x', 'a and b are different fields']]]]),
|
||||
[
|
||||
new SourceLocation(3,9),
|
||||
new SourceLocation(8,9),
|
||||
new SourceLocation(4,11),
|
||||
new SourceLocation(9,11),
|
||||
new SourceLocation(5,13),
|
||||
new SourceLocation(8,9),
|
||||
new SourceLocation(9,11),
|
||||
new SourceLocation(10,13)
|
||||
]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it reports deep conflict to nearest common ancestor
|
||||
*/
|
||||
public function testReportsDeepConflictToNearestCommonAncestor()
|
||||
{
|
||||
$this->expectFailsRule(new OverlappingFieldsCanBeMerged, '
|
||||
@ -349,20 +426,82 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
OverlappingFieldsCanBeMerged::fieldsConflictMessage('deepField', [['x', 'a and b are different fields']]),
|
||||
[
|
||||
new SourceLocation(4,11),
|
||||
new SourceLocation(7,11),
|
||||
new SourceLocation(5,13),
|
||||
new SourceLocation(7,11),
|
||||
new SourceLocation(8,13)
|
||||
]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
// return types must be unambiguous
|
||||
public function testConflictingScalarReturnTypes()
|
||||
// Describe: return types must be unambiguous
|
||||
|
||||
/**
|
||||
* @it conflicting return types which potentially overlap
|
||||
*/
|
||||
public function testConflictingReturnTypesWhichPotentiallyOverlap()
|
||||
{
|
||||
// This is invalid since an object could potentially be both the Object
|
||||
// type IntBox and the interface type NonNullStringBox1. While that
|
||||
// condition does not exist in the current schema, the schema could
|
||||
// expand in the future to allow this. Thus it is invalid.
|
||||
$this->expectFailsRuleWithSchema($this->getTestSchema(), new OverlappingFieldsCanBeMerged, '
|
||||
{
|
||||
someBox {
|
||||
...on IntBox {
|
||||
scalar
|
||||
}
|
||||
...on NonNullStringBox1 {
|
||||
scalar
|
||||
}
|
||||
}
|
||||
}
|
||||
', [
|
||||
FormattedError::create(
|
||||
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
||||
'scalar',
|
||||
'they return conflicting types Int and String!'
|
||||
),
|
||||
[new SourceLocation(5, 15),
|
||||
new SourceLocation(8, 15)]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it compatible return shapes on different return types
|
||||
*/
|
||||
public function testCompatibleReturnShapesOnDifferentReturnTypes()
|
||||
{
|
||||
// In this case `deepBox` returns `SomeBox` in the first usage, and
|
||||
// `StringBox` in the second usage. These return types are not the same!
|
||||
// however this is valid because the return *shapes* are compatible.
|
||||
$this->expectPassesRuleWithSchema($this->getTestSchema(), new OverlappingFieldsCanBeMerged, '
|
||||
{
|
||||
someBox {
|
||||
... on SomeBox {
|
||||
deepBox {
|
||||
unrelatedField
|
||||
}
|
||||
}
|
||||
... on StringBox {
|
||||
deepBox {
|
||||
unrelatedField
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it disallows differing return types despite no overlap
|
||||
*/
|
||||
public function testDisallowsDifferingReturnTypesDespiteNoOverlap()
|
||||
{
|
||||
$this->expectFailsRuleWithSchema($this->getTestSchema(), new OverlappingFieldsCanBeMerged, '
|
||||
{
|
||||
boxUnion {
|
||||
someBox {
|
||||
... on IntBox {
|
||||
scalar
|
||||
}
|
||||
@ -373,17 +512,197 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
}
|
||||
', [
|
||||
FormattedError::create(
|
||||
OverlappingFieldsCanBeMerged::fieldsConflictMessage('scalar', 'they return differing types Int and String'),
|
||||
[ new SourceLocation(5,15), new SourceLocation(8,15) ]
|
||||
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
||||
'scalar',
|
||||
'they return conflicting types Int and String'
|
||||
),
|
||||
[ new SourceLocation(5, 15),
|
||||
new SourceLocation(8, 15)]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it disallows differing return type nullability despite no overlap
|
||||
*/
|
||||
public function testDisallowsDifferingReturnTypeNullabilityDespiteNoOverlap()
|
||||
{
|
||||
$this->expectFailsRuleWithSchema($this->getTestSchema(), new OverlappingFieldsCanBeMerged, '
|
||||
{
|
||||
someBox {
|
||||
... on NonNullStringBox1 {
|
||||
scalar
|
||||
}
|
||||
... on StringBox {
|
||||
scalar
|
||||
}
|
||||
}
|
||||
}
|
||||
', [
|
||||
FormattedError::create(
|
||||
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
||||
'scalar',
|
||||
'they return conflicting types String! and String'
|
||||
),
|
||||
[new SourceLocation(5, 15),
|
||||
new SourceLocation(8, 15)]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it disallows differing return type list despite no overlap
|
||||
*/
|
||||
public function testDisallowsDifferingReturnTypeListDespiteNoOverlap()
|
||||
{
|
||||
$this->expectFailsRuleWithSchema($this->getTestSchema(), new OverlappingFieldsCanBeMerged, '
|
||||
{
|
||||
someBox {
|
||||
... on IntBox {
|
||||
box: listStringBox {
|
||||
scalar
|
||||
}
|
||||
}
|
||||
... on StringBox {
|
||||
box: stringBox {
|
||||
scalar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
', [
|
||||
FormattedError::create(
|
||||
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
||||
'box',
|
||||
'they return conflicting types [StringBox] and StringBox'
|
||||
),
|
||||
[new SourceLocation(5, 15),
|
||||
new SourceLocation(10, 15)]
|
||||
)
|
||||
]);
|
||||
|
||||
|
||||
$this->expectFailsRuleWithSchema($this->getTestSchema(), new OverlappingFieldsCanBeMerged, '
|
||||
{
|
||||
someBox {
|
||||
... on IntBox {
|
||||
box: stringBox {
|
||||
scalar
|
||||
}
|
||||
}
|
||||
... on StringBox {
|
||||
box: listStringBox {
|
||||
scalar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
', [
|
||||
FormattedError::create(
|
||||
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
||||
'box',
|
||||
'they return conflicting types StringBox and [StringBox]'
|
||||
),
|
||||
[new SourceLocation(5, 15),
|
||||
new SourceLocation(10, 15)]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
public function testDisallowsDifferingSubfields()
|
||||
{
|
||||
$this->expectFailsRuleWithSchema($this->getTestSchema(), new OverlappingFieldsCanBeMerged, '
|
||||
{
|
||||
someBox {
|
||||
... on IntBox {
|
||||
box: stringBox {
|
||||
val: scalar
|
||||
val: unrelatedField
|
||||
}
|
||||
}
|
||||
... on StringBox {
|
||||
box: stringBox {
|
||||
val: scalar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
', [
|
||||
|
||||
FormattedError::create(
|
||||
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
||||
'val',
|
||||
'scalar and unrelatedField are different fields'
|
||||
),
|
||||
[new SourceLocation(6, 17),
|
||||
new SourceLocation(7, 17)]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it disallows differing deep return types despite no overlap
|
||||
*/
|
||||
public function testDisallowsDifferingDeepReturnTypesDespiteNoOverlap()
|
||||
{
|
||||
$this->expectFailsRuleWithSchema($this->getTestSchema(), new OverlappingFieldsCanBeMerged, '
|
||||
{
|
||||
someBox {
|
||||
... on IntBox {
|
||||
box: stringBox {
|
||||
scalar
|
||||
}
|
||||
}
|
||||
... on StringBox {
|
||||
box: intBox {
|
||||
scalar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
', [
|
||||
FormattedError::create(
|
||||
OverlappingFieldsCanBeMerged::fieldsConflictMessage(
|
||||
'box',
|
||||
[ [ 'scalar', 'they return conflicting types String and Int' ] ]
|
||||
),
|
||||
[
|
||||
new SourceLocation(5, 15),
|
||||
new SourceLocation(6, 17),
|
||||
new SourceLocation(10, 15),
|
||||
new SourceLocation(11, 17)
|
||||
]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it allows non-conflicting overlaping types
|
||||
*/
|
||||
public function testAllowsNonConflictingOverlapingTypes()
|
||||
{
|
||||
$this->expectPassesRuleWithSchema($this->getTestSchema(), new OverlappingFieldsCanBeMerged, '
|
||||
{
|
||||
someBox {
|
||||
... on IntBox {
|
||||
scalar: unrelatedField
|
||||
}
|
||||
... on StringBox {
|
||||
scalar
|
||||
}
|
||||
}
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it same wrapped scalar return types
|
||||
*/
|
||||
public function testSameWrappedScalarReturnTypes()
|
||||
{
|
||||
$this->expectPassesRuleWithSchema($this->getTestSchema(), new OverlappingFieldsCanBeMerged, '
|
||||
{
|
||||
boxUnion {
|
||||
someBox {
|
||||
...on NonNullStringBox1 {
|
||||
scalar
|
||||
}
|
||||
@ -395,6 +714,24 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it allows inline typeless fragments
|
||||
*/
|
||||
public function testAllowsInlineTypelessFragments()
|
||||
{
|
||||
$this->expectPassesRuleWithSchema($this->getTestSchema(), new OverlappingFieldsCanBeMerged, '
|
||||
{
|
||||
a
|
||||
... {
|
||||
a
|
||||
}
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it compares deep types including list
|
||||
*/
|
||||
public function testComparesDeepTypesIncludingList()
|
||||
{
|
||||
$this->expectFailsRuleWithSchema($this->getTestSchema(), new OverlappingFieldsCanBeMerged, '
|
||||
@ -420,19 +757,25 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
FormattedError::create(
|
||||
OverlappingFieldsCanBeMerged::fieldsConflictMessage('edges', [['node', [['id', 'id and name are different fields']]]]),
|
||||
[
|
||||
new SourceLocation(14, 11), new SourceLocation(5, 13),
|
||||
new SourceLocation(15, 13), new SourceLocation(6, 15),
|
||||
new SourceLocation(16, 15), new SourceLocation(7, 17),
|
||||
new SourceLocation(14, 11),
|
||||
new SourceLocation(15, 13),
|
||||
new SourceLocation(16, 15),
|
||||
new SourceLocation(5, 13),
|
||||
new SourceLocation(6, 15),
|
||||
new SourceLocation(7, 17),
|
||||
]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it ignores unknown types
|
||||
*/
|
||||
public function testIgnoresUnknownTypes()
|
||||
{
|
||||
$this->expectPassesRuleWithSchema($this->getTestSchema(), new OverlappingFieldsCanBeMerged, '
|
||||
{
|
||||
boxUnion {
|
||||
someBox {
|
||||
...on UnknownType {
|
||||
scalar
|
||||
}
|
||||
@ -446,38 +789,86 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
|
||||
private function getTestSchema()
|
||||
{
|
||||
$StringBox = null;
|
||||
$IntBox = null;
|
||||
$SomeBox = null;
|
||||
|
||||
$SomeBox = new InterfaceType([
|
||||
'name' => 'SomeBox',
|
||||
'resolveType' => function() use (&$StringBox) {return $StringBox;},
|
||||
'fields' => function() use (&$SomeBox) {
|
||||
return [
|
||||
'deepBox' => ['type' => $SomeBox],
|
||||
'unrelatedField' => ['type' => Type::string()]
|
||||
];
|
||||
|
||||
}
|
||||
]);
|
||||
|
||||
$StringBox = new ObjectType([
|
||||
'name' => 'StringBox',
|
||||
'fields' => [
|
||||
'scalar' => [ 'type' => Type::string() ]
|
||||
]
|
||||
'interfaces' => [$SomeBox],
|
||||
'fields' => function() use (&$StringBox, &$IntBox) {
|
||||
return [
|
||||
'scalar' => ['type' => Type::string()],
|
||||
'deepBox' => ['type' => $StringBox],
|
||||
'unrelatedField' => ['type' => Type::string()],
|
||||
'listStringBox' => ['type' => Type::listOf($StringBox)],
|
||||
'stringBox' => ['type' => $StringBox],
|
||||
'intBox' => ['type' => $IntBox],
|
||||
];
|
||||
}
|
||||
]);
|
||||
|
||||
$IntBox = new ObjectType([
|
||||
'name' => 'IntBox',
|
||||
'fields' => [
|
||||
'scalar' => ['type' => Type::int() ]
|
||||
]
|
||||
'interfaces' => [$SomeBox],
|
||||
'fields' => function() use (&$StringBox, &$IntBox) {
|
||||
return [
|
||||
'scalar' => ['type' => Type::int()],
|
||||
'deepBox' => ['type' => $IntBox],
|
||||
'unrelatedField' => ['type' => Type::string()],
|
||||
'listStringBox' => ['type' => Type::listOf($StringBox)],
|
||||
'stringBox' => ['type' => $StringBox],
|
||||
'intBox' => ['type' => $IntBox],
|
||||
];
|
||||
}
|
||||
]);
|
||||
|
||||
$NonNullStringBox1 = new ObjectType([
|
||||
$NonNullStringBox1 = new InterfaceType([
|
||||
'name' => 'NonNullStringBox1',
|
||||
'resolveType' => function() use (&$StringBox) {return $StringBox;},
|
||||
'fields' => [
|
||||
'scalar' => [ 'type' => Type::nonNull(Type::string()) ]
|
||||
]
|
||||
]);
|
||||
|
||||
$NonNullStringBox2 = new ObjectType([
|
||||
$NonNullStringBox1Impl = new ObjectType([
|
||||
'name' => 'NonNullStringBox1Impl',
|
||||
'interfaces' => [ $SomeBox, $NonNullStringBox1 ],
|
||||
'fields' => [
|
||||
'scalar' => [ 'type' => Type::nonNull(Type::string()) ],
|
||||
'unrelatedField' => ['type' => Type::string() ],
|
||||
'deepBox' => [ 'type' => $SomeBox ],
|
||||
]
|
||||
]);
|
||||
|
||||
$NonNullStringBox2 = new InterfaceType([
|
||||
'name' => 'NonNullStringBox2',
|
||||
'resolveType' => function() use (&$StringBox) {return $StringBox;},
|
||||
'fields' => [
|
||||
'scalar' => ['type' => Type::nonNull(Type::string())]
|
||||
]
|
||||
]);
|
||||
|
||||
$BoxUnion = new UnionType([
|
||||
'name' => 'BoxUnion',
|
||||
'resolveType' => function() use ($StringBox) {return $StringBox;},
|
||||
'types' => [ $StringBox, $IntBox, $NonNullStringBox1, $NonNullStringBox2 ]
|
||||
$NonNullStringBox2Impl = new ObjectType([
|
||||
'name' => 'NonNullStringBox2Impl',
|
||||
'interfaces' => [ $SomeBox, $NonNullStringBox2 ],
|
||||
'fields' => [
|
||||
'scalar' => [ 'type' => Type::nonNull(Type::string()) ],
|
||||
'unrelatedField' => [ 'type' => Type::string() ],
|
||||
'deepBox' => [ 'type' => $SomeBox ],
|
||||
]
|
||||
]);
|
||||
|
||||
$Connection = new ObjectType([
|
||||
@ -502,13 +893,16 @@ class OverlappingFieldsCanBeMergedTest extends TestCase
|
||||
]
|
||||
]);
|
||||
|
||||
$schema = new Schema(new ObjectType([
|
||||
$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'QueryRoot',
|
||||
'fields' => [
|
||||
'boxUnion' => ['type' => $BoxUnion ],
|
||||
'someBox' => ['type' => $SomeBox],
|
||||
'connection' => ['type' => $Connection]
|
||||
]
|
||||
]));
|
||||
]),
|
||||
'types' => [$IntBox, $StringBox, $NonNullStringBox1Impl, $NonNullStringBox2Impl]
|
||||
]);
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
@ -8,6 +8,10 @@ use GraphQL\Validator\Rules\PossibleFragmentSpreads;
|
||||
class PossibleFragmentSpreadsTest extends TestCase
|
||||
{
|
||||
// Validate: Possible fragment spreads
|
||||
|
||||
/**
|
||||
* @it of the same object
|
||||
*/
|
||||
public function testOfTheSameObject()
|
||||
{
|
||||
$this->expectPassesRule(new PossibleFragmentSpreads(), '
|
||||
@ -16,6 +20,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it of the same object with inline fragment
|
||||
*/
|
||||
public function testOfTheSameObjectWithInlineFragment()
|
||||
{
|
||||
$this->expectPassesRule(new PossibleFragmentSpreads, '
|
||||
@ -23,6 +30,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it object into an implemented interface
|
||||
*/
|
||||
public function testObjectIntoAnImplementedInterface()
|
||||
{
|
||||
$this->expectPassesRule(new PossibleFragmentSpreads, '
|
||||
@ -31,6 +41,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it object into containing union
|
||||
*/
|
||||
public function testObjectIntoContainingUnion()
|
||||
{
|
||||
$this->expectPassesRule(new PossibleFragmentSpreads, '
|
||||
@ -39,6 +52,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it union into contained object
|
||||
*/
|
||||
public function testUnionIntoContainedObject()
|
||||
{
|
||||
$this->expectPassesRule(new PossibleFragmentSpreads, '
|
||||
@ -47,6 +63,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it union into overlapping interface
|
||||
*/
|
||||
public function testUnionIntoOverlappingInterface()
|
||||
{
|
||||
$this->expectPassesRule(new PossibleFragmentSpreads, '
|
||||
@ -55,6 +74,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it union into overlapping union
|
||||
*/
|
||||
public function testUnionIntoOverlappingUnion()
|
||||
{
|
||||
$this->expectPassesRule(new PossibleFragmentSpreads, '
|
||||
@ -63,6 +85,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it interface into implemented object
|
||||
*/
|
||||
public function testInterfaceIntoImplementedObject()
|
||||
{
|
||||
$this->expectPassesRule(new PossibleFragmentSpreads, '
|
||||
@ -71,6 +96,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it interface into overlapping interface
|
||||
*/
|
||||
public function testInterfaceIntoOverlappingInterface()
|
||||
{
|
||||
$this->expectPassesRule(new PossibleFragmentSpreads, '
|
||||
@ -79,6 +107,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it interface into overlapping interface in inline fragment
|
||||
*/
|
||||
public function testInterfaceIntoOverlappingInterfaceInInlineFragment()
|
||||
{
|
||||
$this->expectPassesRule(new PossibleFragmentSpreads, '
|
||||
@ -86,6 +117,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it interface into overlapping union
|
||||
*/
|
||||
public function testInterfaceIntoOverlappingUnion()
|
||||
{
|
||||
$this->expectPassesRule(new PossibleFragmentSpreads, '
|
||||
@ -94,6 +128,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it different object into object
|
||||
*/
|
||||
public function testDifferentObjectIntoObject()
|
||||
{
|
||||
$this->expectFailsRule(new PossibleFragmentSpreads, '
|
||||
@ -104,6 +141,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it different object into object in inline fragment
|
||||
*/
|
||||
public function testDifferentObjectIntoObjectInInlineFragment()
|
||||
{
|
||||
$this->expectFailsRule(new PossibleFragmentSpreads, '
|
||||
@ -115,6 +155,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it object into not implementing interface
|
||||
*/
|
||||
public function testObjectIntoNotImplementingInterface()
|
||||
{
|
||||
$this->expectFailsRule(new PossibleFragmentSpreads, '
|
||||
@ -125,6 +168,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it object into not containing union
|
||||
*/
|
||||
public function testObjectIntoNotContainingUnion()
|
||||
{
|
||||
$this->expectFailsRule(new PossibleFragmentSpreads, '
|
||||
@ -135,6 +181,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it union into not contained object
|
||||
*/
|
||||
public function testUnionIntoNotContainedObject()
|
||||
{
|
||||
$this->expectFailsRule(new PossibleFragmentSpreads, '
|
||||
@ -145,6 +194,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it union into non overlapping interface
|
||||
*/
|
||||
public function testUnionIntoNonOverlappingInterface()
|
||||
{
|
||||
$this->expectFailsRule(new PossibleFragmentSpreads, '
|
||||
@ -155,6 +207,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it union into non overlapping union
|
||||
*/
|
||||
public function testUnionIntoNonOverlappingUnion()
|
||||
{
|
||||
$this->expectFailsRule(new PossibleFragmentSpreads, '
|
||||
@ -165,6 +220,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it interface into non implementing object
|
||||
*/
|
||||
public function testInterfaceIntoNonImplementingObject()
|
||||
{
|
||||
$this->expectFailsRule(new PossibleFragmentSpreads, '
|
||||
@ -175,6 +233,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it interface into non overlapping interface
|
||||
*/
|
||||
public function testInterfaceIntoNonOverlappingInterface()
|
||||
{
|
||||
$this->expectFailsRule(new PossibleFragmentSpreads, '
|
||||
@ -187,6 +248,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it interface into non overlapping interface in inline fragment
|
||||
*/
|
||||
public function testInterfaceIntoNonOverlappingInterfaceInInlineFragment()
|
||||
{
|
||||
$this->expectFailsRule(new PossibleFragmentSpreads, '
|
||||
@ -198,6 +262,9 @@ class PossibleFragmentSpreadsTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it interface into non overlapping union
|
||||
*/
|
||||
public function testInterfaceIntoNonOverlappingUnion()
|
||||
{
|
||||
$this->expectFailsRule(new PossibleFragmentSpreads, '
|
||||
|
@ -8,6 +8,10 @@ use GraphQL\Validator\Rules\ProvidedNonNullArguments;
|
||||
class ProvidedNonNullArgumentsTest extends TestCase
|
||||
{
|
||||
// Validate: Provided required arguments
|
||||
|
||||
/**
|
||||
* @it ignores unknown arguments
|
||||
*/
|
||||
public function testIgnoresUnknownArguments()
|
||||
{
|
||||
// ignores unknown arguments
|
||||
@ -21,6 +25,10 @@ class ProvidedNonNullArgumentsTest extends TestCase
|
||||
}
|
||||
|
||||
// Valid non-nullable value:
|
||||
|
||||
/**
|
||||
* @it Arg on optional arg
|
||||
*/
|
||||
public function testArgOnOptionalArg()
|
||||
{
|
||||
$this->expectPassesRule(new ProvidedNonNullArguments, '
|
||||
@ -32,6 +40,23 @@ class ProvidedNonNullArgumentsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it No Arg on optional arg
|
||||
*/
|
||||
public function testNoArgOnOptionalArg()
|
||||
{
|
||||
$this->expectPassesRule(new ProvidedNonNullArguments, '
|
||||
{
|
||||
dog {
|
||||
isHousetrained
|
||||
}
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Multiple args
|
||||
*/
|
||||
public function testMultipleArgs()
|
||||
{
|
||||
$this->expectPassesRule(new ProvidedNonNullArguments, '
|
||||
@ -43,6 +68,9 @@ class ProvidedNonNullArgumentsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Multiple args reverse order
|
||||
*/
|
||||
public function testMultipleArgsReverseOrder()
|
||||
{
|
||||
$this->expectPassesRule(new ProvidedNonNullArguments, '
|
||||
@ -54,6 +82,9 @@ class ProvidedNonNullArgumentsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it No args on multiple optional
|
||||
*/
|
||||
public function testNoArgsOnMultipleOptional()
|
||||
{
|
||||
$this->expectPassesRule(new ProvidedNonNullArguments, '
|
||||
@ -65,6 +96,9 @@ class ProvidedNonNullArgumentsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it One arg on multiple optional
|
||||
*/
|
||||
public function testOneArgOnMultipleOptional()
|
||||
{
|
||||
$this->expectPassesRule(new ProvidedNonNullArguments, '
|
||||
@ -76,6 +110,9 @@ class ProvidedNonNullArgumentsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Second arg on multiple optional
|
||||
*/
|
||||
public function testSecondArgOnMultipleOptional()
|
||||
{
|
||||
$this->expectPassesRule(new ProvidedNonNullArguments, '
|
||||
@ -87,6 +124,9 @@ class ProvidedNonNullArgumentsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Multiple reqs on mixedList
|
||||
*/
|
||||
public function testMultipleReqsOnMixedList()
|
||||
{
|
||||
$this->expectPassesRule(new ProvidedNonNullArguments, '
|
||||
@ -98,6 +138,9 @@ class ProvidedNonNullArgumentsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Multiple reqs and one opt on mixedList
|
||||
*/
|
||||
public function testMultipleReqsAndOneOptOnMixedList()
|
||||
{
|
||||
$this->expectPassesRule(new ProvidedNonNullArguments, '
|
||||
@ -109,6 +152,9 @@ class ProvidedNonNullArgumentsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it All reqs and opts on mixedList
|
||||
*/
|
||||
public function testAllReqsAndOptsOnMixedList()
|
||||
{
|
||||
$this->expectPassesRule(new ProvidedNonNullArguments, '
|
||||
@ -121,6 +167,10 @@ class ProvidedNonNullArgumentsTest extends TestCase
|
||||
}
|
||||
|
||||
// Invalid non-nullable value
|
||||
|
||||
/**
|
||||
* @it Missing one non-nullable argument
|
||||
*/
|
||||
public function testMissingOneNonNullableArgument()
|
||||
{
|
||||
$this->expectFailsRule(new ProvidedNonNullArguments, '
|
||||
@ -134,6 +184,9 @@ class ProvidedNonNullArgumentsTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Missing multiple non-nullable arguments
|
||||
*/
|
||||
public function testMissingMultipleNonNullableArguments()
|
||||
{
|
||||
$this->expectFailsRule(new ProvidedNonNullArguments, '
|
||||
@ -148,6 +201,9 @@ class ProvidedNonNullArgumentsTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Incorrect value and missing argument
|
||||
*/
|
||||
public function testIncorrectValueAndMissingArgument()
|
||||
{
|
||||
$this->expectFailsRule(new ProvidedNonNullArguments, '
|
||||
@ -161,7 +217,11 @@ class ProvidedNonNullArgumentsTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
// Directive arguments
|
||||
// Describe: Directive arguments
|
||||
|
||||
/**
|
||||
* @it ignores unknown directives
|
||||
*/
|
||||
public function testIgnoresUnknownDirectives()
|
||||
{
|
||||
$this->expectPassesRule(new ProvidedNonNullArguments, '
|
||||
@ -171,6 +231,9 @@ class ProvidedNonNullArgumentsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it with directives of valid types
|
||||
*/
|
||||
public function testWithDirectivesOfValidTypes()
|
||||
{
|
||||
$this->expectPassesRule(new ProvidedNonNullArguments, '
|
||||
@ -185,6 +248,9 @@ class ProvidedNonNullArgumentsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it with directive with missing types
|
||||
*/
|
||||
public function testWithDirectiveWithMissingTypes()
|
||||
{
|
||||
$this->expectFailsRule(new ProvidedNonNullArguments, '
|
||||
|
@ -24,7 +24,9 @@ class QuerySecuritySchema
|
||||
return self::$schema;
|
||||
}
|
||||
|
||||
self::$schema = new Schema(static::buildQueryRootType());
|
||||
self::$schema = new Schema([
|
||||
'query' => static::buildQueryRootType()
|
||||
]);
|
||||
|
||||
return self::$schema;
|
||||
}
|
||||
|
@ -9,6 +9,9 @@ class ScalarLeafsTest extends TestCase
|
||||
{
|
||||
// Validate: Scalar leafs
|
||||
|
||||
/**
|
||||
* @it valid scalar selection
|
||||
*/
|
||||
public function testValidScalarSelection()
|
||||
{
|
||||
$this->expectPassesRule(new ScalarLeafs, '
|
||||
@ -18,6 +21,9 @@ class ScalarLeafsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it object type missing selection
|
||||
*/
|
||||
public function testObjectTypeMissingSelection()
|
||||
{
|
||||
$this->expectFailsRule(new ScalarLeafs, '
|
||||
@ -27,6 +33,9 @@ class ScalarLeafsTest extends TestCase
|
||||
', [$this->missingObjSubselection('human', 'Human', 3, 9)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it interface type missing selection
|
||||
*/
|
||||
public function testInterfaceTypeMissingSelection()
|
||||
{
|
||||
$this->expectFailsRule(new ScalarLeafs, '
|
||||
@ -36,6 +45,9 @@ class ScalarLeafsTest extends TestCase
|
||||
', [$this->missingObjSubselection('pets', '[Pet]', 3, 17)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it valid scalar selection with args
|
||||
*/
|
||||
public function testValidScalarSelectionWithArgs()
|
||||
{
|
||||
$this->expectPassesRule(new ScalarLeafs, '
|
||||
@ -45,6 +57,9 @@ class ScalarLeafsTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it scalar selection not allowed on Boolean
|
||||
*/
|
||||
public function testScalarSelectionNotAllowedOnBoolean()
|
||||
{
|
||||
$this->expectFailsRule(new ScalarLeafs, '
|
||||
@ -55,6 +70,9 @@ class ScalarLeafsTest extends TestCase
|
||||
[$this->noScalarSubselection('barks', 'Boolean', 3, 15)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it scalar selection not allowed on Enum
|
||||
*/
|
||||
public function testScalarSelectionNotAllowedOnEnum()
|
||||
{
|
||||
$this->expectFailsRule(new ScalarLeafs, '
|
||||
@ -66,6 +84,9 @@ class ScalarLeafsTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it scalar selection not allowed with args
|
||||
*/
|
||||
public function testScalarSelectionNotAllowedWithArgs()
|
||||
{
|
||||
$this->expectFailsRule(new ScalarLeafs, '
|
||||
@ -77,6 +98,9 @@ class ScalarLeafsTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Scalar selection not allowed with directives
|
||||
*/
|
||||
public function testScalarSelectionNotAllowedWithDirectives()
|
||||
{
|
||||
$this->expectFailsRule(new ScalarLeafs, '
|
||||
@ -88,6 +112,9 @@ class ScalarLeafsTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Scalar selection not allowed with directives and args
|
||||
*/
|
||||
public function testScalarSelectionNotAllowedWithDirectivesAndArgs()
|
||||
{
|
||||
$this->expectFailsRule(new ScalarLeafs, '
|
||||
|
186
tests/Validator/UniqueArgumentNamesTest.php
Normal file
186
tests/Validator/UniqueArgumentNamesTest.php
Normal file
@ -0,0 +1,186 @@
|
||||
<?php
|
||||
namespace GraphQL\Tests\Validator;
|
||||
|
||||
use GraphQL\FormattedError;
|
||||
use GraphQL\Language\SourceLocation;
|
||||
use GraphQL\Validator\Rules\UniqueArgumentNames;
|
||||
|
||||
class UniqueArgumentNamesTest extends TestCase
|
||||
{
|
||||
// Validate: Unique argument names
|
||||
|
||||
/**
|
||||
* @it no arguments on field
|
||||
*/
|
||||
public function testNoArgumentsOnField()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueArgumentNames(), '
|
||||
{
|
||||
field
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it no arguments on directive
|
||||
*/
|
||||
public function testNoArgumentsOnDirective()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueArgumentNames, '
|
||||
{
|
||||
field @directive
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it argument on field
|
||||
*/
|
||||
public function testArgumentOnField()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueArgumentNames, '
|
||||
{
|
||||
field(arg: "value")
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it argument on directive
|
||||
*/
|
||||
public function testArgumentOnDirective()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueArgumentNames, '
|
||||
{
|
||||
field @directive(arg: "value")
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it same argument on two fields
|
||||
*/
|
||||
public function testSameArgumentOnTwoFields()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueArgumentNames, '
|
||||
{
|
||||
one: field(arg: "value")
|
||||
two: field(arg: "value")
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it same argument on field and directive
|
||||
*/
|
||||
public function testSameArgumentOnFieldAndDirective()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueArgumentNames, '
|
||||
{
|
||||
field(arg: "value") @directive(arg: "value")
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it same argument on two directives
|
||||
*/
|
||||
public function testSameArgumentOnTwoDirectives()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueArgumentNames, '
|
||||
{
|
||||
field @directive1(arg: "value") @directive2(arg: "value")
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it multiple field arguments
|
||||
*/
|
||||
public function testMultipleFieldArguments()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueArgumentNames, '
|
||||
{
|
||||
field(arg1: "value", arg2: "value", arg3: "value")
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it multiple directive arguments
|
||||
*/
|
||||
public function testMultipleDirectiveArguments()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueArgumentNames, '
|
||||
{
|
||||
field @directive(arg1: "value", arg2: "value", arg3: "value")
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it duplicate field arguments
|
||||
*/
|
||||
public function testDuplicateFieldArguments()
|
||||
{
|
||||
$this->expectFailsRule(new UniqueArgumentNames, '
|
||||
{
|
||||
field(arg1: "value", arg1: "value")
|
||||
}
|
||||
', [
|
||||
$this->duplicateArg('arg1', 3, 15, 3, 30)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it many duplicate field arguments
|
||||
*/
|
||||
public function testManyDuplicateFieldArguments()
|
||||
{
|
||||
$this->expectFailsRule(new UniqueArgumentNames, '
|
||||
{
|
||||
field(arg1: "value", arg1: "value", arg1: "value")
|
||||
}
|
||||
', [
|
||||
$this->duplicateArg('arg1', 3, 15, 3, 30),
|
||||
$this->duplicateArg('arg1', 3, 15, 3, 45)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it duplicate directive arguments
|
||||
*/
|
||||
public function testDuplicateDirectiveArguments()
|
||||
{
|
||||
$this->expectFailsRule(new UniqueArgumentNames, '
|
||||
{
|
||||
field @directive(arg1: "value", arg1: "value")
|
||||
}
|
||||
', [
|
||||
$this->duplicateArg('arg1', 3, 26, 3, 41)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it many duplicate directive arguments
|
||||
*/
|
||||
public function testManyDuplicateDirectiveArguments()
|
||||
{
|
||||
$this->expectFailsRule(new UniqueArgumentNames, '
|
||||
{
|
||||
field @directive(arg1: "value", arg1: "value", arg1: "value")
|
||||
}
|
||||
', [
|
||||
$this->duplicateArg('arg1', 3, 26, 3, 41),
|
||||
$this->duplicateArg('arg1', 3, 26, 3, 56)
|
||||
]);
|
||||
}
|
||||
|
||||
private function duplicateArg($argName, $l1, $c1, $l2, $c2)
|
||||
{
|
||||
return FormattedError::create(
|
||||
UniqueArgumentNames::duplicateArgMessage($argName),
|
||||
[new SourceLocation($l1, $c1), new SourceLocation($l2, $c2)]
|
||||
);
|
||||
}
|
||||
}
|
139
tests/Validator/UniqueFragmentNamesTest.php
Normal file
139
tests/Validator/UniqueFragmentNamesTest.php
Normal file
@ -0,0 +1,139 @@
|
||||
<?php
|
||||
namespace GraphQL\Tests\Validator;
|
||||
|
||||
use GraphQL\FormattedError;
|
||||
use GraphQL\Language\SourceLocation;
|
||||
use GraphQL\Validator\Rules\UniqueFragmentNames;
|
||||
|
||||
class UniqueFragmentNamesTest extends TestCase
|
||||
{
|
||||
// Validate: Unique fragment names
|
||||
|
||||
/**
|
||||
* @it no fragments
|
||||
*/
|
||||
public function testNoFragments()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueFragmentNames(), '
|
||||
{
|
||||
field
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it one fragment
|
||||
*/
|
||||
public function testOneFragment()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueFragmentNames, '
|
||||
{
|
||||
...fragA
|
||||
}
|
||||
|
||||
fragment fragA on Type {
|
||||
field
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it many fragments
|
||||
*/
|
||||
public function testManyFragments()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueFragmentNames, '
|
||||
{
|
||||
...fragA
|
||||
...fragB
|
||||
...fragC
|
||||
}
|
||||
fragment fragA on Type {
|
||||
fieldA
|
||||
}
|
||||
fragment fragB on Type {
|
||||
fieldB
|
||||
}
|
||||
fragment fragC on Type {
|
||||
fieldC
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it inline fragments are always unique
|
||||
*/
|
||||
public function testInlineFragmentsAreAlwaysUnique()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueFragmentNames, '
|
||||
{
|
||||
...on Type {
|
||||
fieldA
|
||||
}
|
||||
...on Type {
|
||||
fieldB
|
||||
}
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it fragment and operation named the same
|
||||
*/
|
||||
public function testFragmentAndOperationNamedTheSame()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueFragmentNames, '
|
||||
query Foo {
|
||||
...Foo
|
||||
}
|
||||
fragment Foo on Type {
|
||||
field
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it fragments named the same
|
||||
*/
|
||||
public function testFragmentsNamedTheSame()
|
||||
{
|
||||
$this->expectFailsRule(new UniqueFragmentNames, '
|
||||
{
|
||||
...fragA
|
||||
}
|
||||
fragment fragA on Type {
|
||||
fieldA
|
||||
}
|
||||
fragment fragA on Type {
|
||||
fieldB
|
||||
}
|
||||
', [
|
||||
$this->duplicateFrag('fragA', 5, 16, 8, 16)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it fragments named the same without being referenced
|
||||
*/
|
||||
public function testFragmentsNamedTheSameWithoutBeingReferenced()
|
||||
{
|
||||
$this->expectFailsRule(new UniqueFragmentNames, '
|
||||
fragment fragA on Type {
|
||||
fieldA
|
||||
}
|
||||
fragment fragA on Type {
|
||||
fieldB
|
||||
}
|
||||
', [
|
||||
$this->duplicateFrag('fragA', 2, 16, 5, 16)
|
||||
]);
|
||||
}
|
||||
|
||||
private function duplicateFrag($fragName, $l1, $c1, $l2, $c2)
|
||||
{
|
||||
return FormattedError::create(
|
||||
UniqueFragmentNames::duplicateFragmentNameMessage($fragName),
|
||||
[new SourceLocation($l1, $c1), new SourceLocation($l2, $c2)]
|
||||
);
|
||||
}
|
||||
}
|
104
tests/Validator/UniqueInputFieldNamesTest.php
Normal file
104
tests/Validator/UniqueInputFieldNamesTest.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
namespace GraphQL\Tests\Validator;
|
||||
|
||||
use GraphQL\FormattedError;
|
||||
use GraphQL\Language\SourceLocation;
|
||||
use GraphQL\Validator\Rules\UniqueInputFieldNames;
|
||||
|
||||
class UniqueInputFieldNamesTest extends TestCase
|
||||
{
|
||||
// Validate: Unique input field names
|
||||
|
||||
/**
|
||||
* @it input object with fields
|
||||
*/
|
||||
public function testInputObjectWithFields()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueInputFieldNames(), '
|
||||
{
|
||||
field(arg: { f: true })
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it same input object within two args
|
||||
*/
|
||||
public function testSameInputObjectWithinTwoArgs()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueInputFieldNames, '
|
||||
{
|
||||
field(arg1: { f: true }, arg2: { f: true })
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it multiple input object fields
|
||||
*/
|
||||
public function testMultipleInputObjectFields()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueInputFieldNames, '
|
||||
{
|
||||
field(arg: { f1: "value", f2: "value", f3: "value" })
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it allows for nested input objects with similar fields
|
||||
*/
|
||||
public function testAllowsForNestedInputObjectsWithSimilarFields()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueInputFieldNames, '
|
||||
{
|
||||
field(arg: {
|
||||
deep: {
|
||||
deep: {
|
||||
id: 1
|
||||
}
|
||||
id: 1
|
||||
}
|
||||
id: 1
|
||||
})
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it duplicate input object fields
|
||||
*/
|
||||
public function testDuplicateInputObjectFields()
|
||||
{
|
||||
$this->expectFailsRule(new UniqueInputFieldNames, '
|
||||
{
|
||||
field(arg: { f1: "value", f1: "value" })
|
||||
}
|
||||
', [
|
||||
$this->duplicateField('f1', 3, 22, 3, 35)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it many duplicate input object fields
|
||||
*/
|
||||
public function testManyDuplicateInputObjectFields()
|
||||
{
|
||||
$this->expectFailsRule(new UniqueInputFieldNames, '
|
||||
{
|
||||
field(arg: { f1: "value", f1: "value", f1: "value" })
|
||||
}
|
||||
', [
|
||||
$this->duplicateField('f1', 3, 22, 3, 35),
|
||||
$this->duplicateField('f1', 3, 22, 3, 48)
|
||||
]);
|
||||
}
|
||||
|
||||
private function duplicateField($name, $l1, $c1, $l2, $c2)
|
||||
{
|
||||
return FormattedError::create(
|
||||
UniqueInputFieldNames::duplicateInputFieldMessage($name),
|
||||
[new SourceLocation($l1, $c1), new SourceLocation($l2, $c2)]
|
||||
);
|
||||
}
|
||||
}
|
157
tests/Validator/UniqueOperationNamesTest.php
Normal file
157
tests/Validator/UniqueOperationNamesTest.php
Normal file
@ -0,0 +1,157 @@
|
||||
<?php
|
||||
namespace GraphQL\Tests\Validator;
|
||||
|
||||
use GraphQL\FormattedError;
|
||||
use GraphQL\Language\SourceLocation;
|
||||
use GraphQL\Validator\Rules\UniqueOperationNames;
|
||||
|
||||
class UniqueOperationNamesTest extends TestCase
|
||||
{
|
||||
// Validate: Unique operation names
|
||||
|
||||
/**
|
||||
* @it no operations
|
||||
*/
|
||||
public function testNoOperations()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueOperationNames(), '
|
||||
fragment fragA on Type {
|
||||
field
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it one anon operation
|
||||
*/
|
||||
public function testOneAnonOperation()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueOperationNames, '
|
||||
{
|
||||
field
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it one named operation
|
||||
*/
|
||||
public function testOneNamedOperation()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueOperationNames, '
|
||||
query Foo {
|
||||
field
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it multiple operations
|
||||
*/
|
||||
public function testMultipleOperations()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueOperationNames, '
|
||||
query Foo {
|
||||
field
|
||||
}
|
||||
|
||||
query Bar {
|
||||
field
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it multiple operations of different types
|
||||
*/
|
||||
public function testMultipleOperationsOfDifferentTypes()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueOperationNames, '
|
||||
query Foo {
|
||||
field
|
||||
}
|
||||
|
||||
mutation Bar {
|
||||
field
|
||||
}
|
||||
|
||||
subscription Baz {
|
||||
field
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it fragment and operation named the same
|
||||
*/
|
||||
public function testFragmentAndOperationNamedTheSame()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueOperationNames, '
|
||||
query Foo {
|
||||
...Foo
|
||||
}
|
||||
fragment Foo on Type {
|
||||
field
|
||||
}
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it multiple operations of same name
|
||||
*/
|
||||
public function testMultipleOperationsOfSameName()
|
||||
{
|
||||
$this->expectFailsRule(new UniqueOperationNames, '
|
||||
query Foo {
|
||||
fieldA
|
||||
}
|
||||
query Foo {
|
||||
fieldB
|
||||
}
|
||||
', [
|
||||
$this->duplicateOp('Foo', 2, 13, 5, 13)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it multiple ops of same name of different types (mutation)
|
||||
*/
|
||||
public function testMultipleOpsOfSameNameOfDifferentTypes_Mutation()
|
||||
{
|
||||
$this->expectFailsRule(new UniqueOperationNames, '
|
||||
query Foo {
|
||||
fieldA
|
||||
}
|
||||
mutation Foo {
|
||||
fieldB
|
||||
}
|
||||
', [
|
||||
$this->duplicateOp('Foo', 2, 13, 5, 16)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it multiple ops of same name of different types (subscription)
|
||||
*/
|
||||
public function testMultipleOpsOfSameNameOfDifferentTypes_Subscription()
|
||||
{
|
||||
$this->expectFailsRule(new UniqueOperationNames, '
|
||||
query Foo {
|
||||
fieldA
|
||||
}
|
||||
subscription Foo {
|
||||
fieldB
|
||||
}
|
||||
', [
|
||||
$this->duplicateOp('Foo', 2, 13, 5, 20)
|
||||
]);
|
||||
}
|
||||
|
||||
private function duplicateOp($opName, $l1, $c1, $l2, $c2)
|
||||
{
|
||||
return FormattedError::create(
|
||||
UniqueOperationNames::duplicateOperationNameMessage($opName),
|
||||
[new SourceLocation($l1, $c1), new SourceLocation($l2, $c2)]
|
||||
);
|
||||
}
|
||||
}
|
47
tests/Validator/UniqueVariableNamesTest.php
Normal file
47
tests/Validator/UniqueVariableNamesTest.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
namespace GraphQL\Tests\Validator;
|
||||
|
||||
use GraphQL\FormattedError;
|
||||
use GraphQL\Language\SourceLocation;
|
||||
use GraphQL\Validator\Rules\UniqueVariableNames;
|
||||
|
||||
class UniqueVariableNamesTest extends TestCase
|
||||
{
|
||||
// Validate: Unique variable names
|
||||
|
||||
/**
|
||||
* @it unique variable names
|
||||
*/
|
||||
public function testUniqueVariableNames()
|
||||
{
|
||||
$this->expectPassesRule(new UniqueVariableNames(), '
|
||||
query A($x: Int, $y: String) { __typename }
|
||||
query B($x: String, $y: Int) { __typename }
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it duplicate variable names
|
||||
*/
|
||||
public function testDuplicateVariableNames()
|
||||
{
|
||||
$this->expectFailsRule(new UniqueVariableNames, '
|
||||
query A($x: Int, $x: Int, $x: String) { __typename }
|
||||
query B($x: String, $x: Int) { __typename }
|
||||
query C($x: Int, $x: Int) { __typename }
|
||||
', [
|
||||
$this->duplicateVariable('x', 2, 16, 2, 25),
|
||||
$this->duplicateVariable('x', 2, 16, 2, 34),
|
||||
$this->duplicateVariable('x', 3, 16, 3, 28),
|
||||
$this->duplicateVariable('x', 4, 16, 4, 25)
|
||||
]);
|
||||
}
|
||||
|
||||
private function duplicateVariable($name, $l1, $c1, $l2, $c2)
|
||||
{
|
||||
return FormattedError::create(
|
||||
UniqueVariableNames::duplicateVariableMessage($name),
|
||||
[new SourceLocation($l1, $c1), new SourceLocation($l2, $c2)]
|
||||
);
|
||||
}
|
||||
}
|
@ -8,6 +8,10 @@ use GraphQL\Validator\Rules\VariablesAreInputTypes;
|
||||
class VariablesAreInputTypesTest extends TestCase
|
||||
{
|
||||
// Validate: Variables are input types
|
||||
|
||||
/**
|
||||
* @it input types are valid
|
||||
*/
|
||||
public function testInputTypesAreValid()
|
||||
{
|
||||
$this->expectPassesRule(new VariablesAreInputTypes(), '
|
||||
@ -17,6 +21,9 @@ class VariablesAreInputTypesTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it output types are invalid
|
||||
*/
|
||||
public function testOutputTypesAreInvalid()
|
||||
{
|
||||
$this->expectFailsRule(new VariablesAreInputTypes, '
|
||||
|
@ -10,6 +10,9 @@ class VariablesInAllowedPositionTest extends TestCase
|
||||
{
|
||||
// Validate: Variables are in allowed positions
|
||||
|
||||
/**
|
||||
* @it Boolean => Boolean
|
||||
*/
|
||||
public function testBooleanXBoolean()
|
||||
{
|
||||
// Boolean => Boolean
|
||||
@ -23,6 +26,9 @@ class VariablesInAllowedPositionTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Boolean => Boolean within fragment
|
||||
*/
|
||||
public function testBooleanXBooleanWithinFragment()
|
||||
{
|
||||
// Boolean => Boolean within fragment
|
||||
@ -51,6 +57,9 @@ class VariablesInAllowedPositionTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Boolean! => Boolean
|
||||
*/
|
||||
public function testBooleanNonNullXBoolean()
|
||||
{
|
||||
// Boolean! => Boolean
|
||||
@ -64,6 +73,9 @@ class VariablesInAllowedPositionTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Boolean! => Boolean within fragment
|
||||
*/
|
||||
public function testBooleanNonNullXBooleanWithinFragment()
|
||||
{
|
||||
// Boolean! => Boolean within fragment
|
||||
@ -81,6 +93,9 @@ class VariablesInAllowedPositionTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Int => Int! with default
|
||||
*/
|
||||
public function testIntXIntNonNullWithDefault()
|
||||
{
|
||||
// Int => Int! with default
|
||||
@ -94,9 +109,11 @@ class VariablesInAllowedPositionTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it [String] => [String]
|
||||
*/
|
||||
public function testListOfStringXListOfString()
|
||||
{
|
||||
// [String] => [String]
|
||||
$this->expectPassesRule(new VariablesInAllowedPosition, '
|
||||
query Query($stringListVar: [String])
|
||||
{
|
||||
@ -107,9 +124,11 @@ class VariablesInAllowedPositionTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it [String!] => [String]
|
||||
*/
|
||||
public function testListOfStringNonNullXListOfString()
|
||||
{
|
||||
// [String!] => [String]
|
||||
$this->expectPassesRule(new VariablesInAllowedPosition, '
|
||||
query Query($stringListVar: [String!])
|
||||
{
|
||||
@ -120,9 +139,11 @@ class VariablesInAllowedPositionTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it String => [String] in item position
|
||||
*/
|
||||
public function testStringXListOfStringInItemPosition()
|
||||
{
|
||||
// String => [String] in item position
|
||||
$this->expectPassesRule(new VariablesInAllowedPosition, '
|
||||
query Query($stringVar: String)
|
||||
{
|
||||
@ -133,9 +154,11 @@ class VariablesInAllowedPositionTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it String! => [String] in item position
|
||||
*/
|
||||
public function testStringNonNullXListOfStringInItemPosition()
|
||||
{
|
||||
// String! => [String] in item position
|
||||
$this->expectPassesRule(new VariablesInAllowedPosition, '
|
||||
query Query($stringVar: String!)
|
||||
{
|
||||
@ -146,9 +169,11 @@ class VariablesInAllowedPositionTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it ComplexInput => ComplexInput
|
||||
*/
|
||||
public function testComplexInputXComplexInput()
|
||||
{
|
||||
// ComplexInput => ComplexInput
|
||||
$this->expectPassesRule(new VariablesInAllowedPosition, '
|
||||
query Query($complexVar: ComplexInput)
|
||||
{
|
||||
@ -159,9 +184,11 @@ class VariablesInAllowedPositionTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it ComplexInput => ComplexInput in field position
|
||||
*/
|
||||
public function testComplexInputXComplexInputInFieldPosition()
|
||||
{
|
||||
// ComplexInput => ComplexInput in field position
|
||||
$this->expectPassesRule(new VariablesInAllowedPosition, '
|
||||
query Query($boolVar: Boolean = false)
|
||||
{
|
||||
@ -172,9 +199,11 @@ class VariablesInAllowedPositionTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Boolean! => Boolean! in directive
|
||||
*/
|
||||
public function testBooleanNonNullXBooleanNonNullInDirective()
|
||||
{
|
||||
// Boolean! => Boolean! in directive
|
||||
$this->expectPassesRule(new VariablesInAllowedPosition, '
|
||||
query Query($boolVar: Boolean!)
|
||||
{
|
||||
@ -183,9 +212,11 @@ class VariablesInAllowedPositionTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Boolean => Boolean! in directive with default
|
||||
*/
|
||||
public function testBooleanXBooleanNonNullInDirectiveWithDefault()
|
||||
{
|
||||
// Boolean => Boolean! in directive with default
|
||||
$this->expectPassesRule(new VariablesInAllowedPosition, '
|
||||
query Query($boolVar: Boolean = false)
|
||||
{
|
||||
@ -194,46 +225,51 @@ class VariablesInAllowedPositionTest extends TestCase
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Int => Int!
|
||||
*/
|
||||
public function testIntXIntNonNull()
|
||||
{
|
||||
// Int => Int!
|
||||
$this->expectFailsRule(new VariablesInAllowedPosition, '
|
||||
query Query($intArg: Int)
|
||||
{
|
||||
query Query($intArg: Int) {
|
||||
complicatedArgs {
|
||||
nonNullIntArgField(nonNullIntArg: $intArg)
|
||||
}
|
||||
}
|
||||
', [
|
||||
FormattedError::create(
|
||||
Messages::badVarPosMessage('intArg', 'Int', 'Int!'),
|
||||
[new SourceLocation(5, 45)]
|
||||
VariablesInAllowedPosition::badVarPosMessage('intArg', 'Int', 'Int!'),
|
||||
[new SourceLocation(2, 19), new SourceLocation(4, 45)]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Int => Int! within fragment
|
||||
*/
|
||||
public function testIntXIntNonNullWithinFragment()
|
||||
{
|
||||
// Int => Int! within fragment
|
||||
$this->expectFailsRule(new VariablesInAllowedPosition, '
|
||||
fragment nonNullIntArgFieldFrag on ComplicatedArgs {
|
||||
nonNullIntArgField(nonNullIntArg: $intArg)
|
||||
}
|
||||
|
||||
query Query($intArg: Int)
|
||||
{
|
||||
query Query($intArg: Int) {
|
||||
complicatedArgs {
|
||||
...nonNullIntArgFieldFrag
|
||||
}
|
||||
}
|
||||
', [
|
||||
FormattedError::create(
|
||||
Messages::badVarPosMessage('intArg', 'Int', 'Int!'),
|
||||
[new SourceLocation(3, 43)]
|
||||
VariablesInAllowedPosition::badVarPosMessage('intArg', 'Int', 'Int!'),
|
||||
[new SourceLocation(6, 19), new SourceLocation(3, 43)]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Int => Int! within nested fragment
|
||||
*/
|
||||
public function testIntXIntNonNullWithinNestedFragment()
|
||||
{
|
||||
// Int => Int! within nested fragment
|
||||
@ -254,76 +290,81 @@ class VariablesInAllowedPositionTest extends TestCase
|
||||
}
|
||||
', [
|
||||
FormattedError::create(
|
||||
Messages::badVarPosMessage('intArg', 'Int', 'Int!'),
|
||||
[new SourceLocation(7,43)]
|
||||
VariablesInAllowedPosition::badVarPosMessage('intArg', 'Int', 'Int!'),
|
||||
[new SourceLocation(10, 19), new SourceLocation(7,43)]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it String over Boolean
|
||||
*/
|
||||
public function testStringOverBoolean()
|
||||
{
|
||||
// String over Boolean
|
||||
$this->expectFailsRule(new VariablesInAllowedPosition, '
|
||||
query Query($stringVar: String)
|
||||
{
|
||||
query Query($stringVar: String) {
|
||||
complicatedArgs {
|
||||
booleanArgField(booleanArg: $stringVar)
|
||||
}
|
||||
}
|
||||
', [
|
||||
FormattedError::create(
|
||||
Messages::badVarPosMessage('stringVar', 'String', 'Boolean'),
|
||||
[new SourceLocation(5,39)]
|
||||
VariablesInAllowedPosition::badVarPosMessage('stringVar', 'String', 'Boolean'),
|
||||
[new SourceLocation(2,19), new SourceLocation(4,39)]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it String => [String]
|
||||
*/
|
||||
public function testStringXListOfString()
|
||||
{
|
||||
// String => [String]
|
||||
$this->expectFailsRule(new VariablesInAllowedPosition, '
|
||||
query Query($stringVar: String)
|
||||
{
|
||||
query Query($stringVar: String) {
|
||||
complicatedArgs {
|
||||
stringListArgField(stringListArg: $stringVar)
|
||||
}
|
||||
}
|
||||
', [
|
||||
FormattedError::create(
|
||||
Messages::badVarPosMessage('stringVar', 'String', '[String]'),
|
||||
[new SourceLocation(5,45)]
|
||||
VariablesInAllowedPosition::badVarPosMessage('stringVar', 'String', '[String]'),
|
||||
[new SourceLocation(2, 19), new SourceLocation(4,45)]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Boolean => Boolean! in directive
|
||||
*/
|
||||
public function testBooleanXBooleanNonNullInDirective()
|
||||
{
|
||||
// Boolean => Boolean! in directive
|
||||
$this->expectFailsRule(new VariablesInAllowedPosition, '
|
||||
query Query($boolVar: Boolean)
|
||||
{
|
||||
query Query($boolVar: Boolean) {
|
||||
dog @include(if: $boolVar)
|
||||
}
|
||||
', [
|
||||
FormattedError::create(
|
||||
Messages::badVarPosMessage('boolVar', 'Boolean', 'Boolean!'),
|
||||
[new SourceLocation(4,26)]
|
||||
VariablesInAllowedPosition::badVarPosMessage('boolVar', 'Boolean', 'Boolean!'),
|
||||
[new SourceLocation(2, 19), new SourceLocation(3,26)]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it String => Boolean! in directive
|
||||
*/
|
||||
public function testStringXBooleanNonNullInDirective()
|
||||
{
|
||||
// String => Boolean! in directive
|
||||
$this->expectFailsRule(new VariablesInAllowedPosition, '
|
||||
query Query($stringVar: String)
|
||||
{
|
||||
query Query($stringVar: String) {
|
||||
dog @include(if: $stringVar)
|
||||
}
|
||||
', [
|
||||
FormattedError::create(
|
||||
Messages::badVarPosMessage('stringVar', 'String', 'Boolean!'),
|
||||
[new SourceLocation(4,26)]
|
||||
VariablesInAllowedPosition::badVarPosMessage('stringVar', 'String', 'Boolean!'),
|
||||
[new SourceLocation(2, 19), new SourceLocation(3,26)]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user