Continue updating validator rules for april2016 spec

This commit is contained in:
vladar 2016-04-25 19:29:17 +06:00
parent 8ab7a9a438
commit 800d8ba25f
29 changed files with 1937 additions and 508 deletions

View File

@ -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([

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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;
}
];
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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))
);
}
},
],

View File

@ -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))
);
}
},
],

View 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;
}
];
}
}

View 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;
}
];
}
}

View 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();
}
];
}
}

View 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();
}
];
}
}

View 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;
}
}
];
}
}

View File

@ -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;
}
];
}

View File

@ -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 {

View File

@ -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)]
);
}

View File

@ -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;
}

View File

@ -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, '

View File

@ -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, '

View File

@ -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;
}

View File

@ -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, '

View 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)]
);
}
}

View 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)]
);
}
}

View 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)]
);
}
}

View 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)]
);
}
}

View 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)]
);
}
}

View File

@ -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, '

View File

@ -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)]
)
]);
}