From 800d8ba25f98eb1073ed5afe26b0836292348f04 Mon Sep 17 00:00:00 2001 From: vladar Date: Mon, 25 Apr 2016 19:29:17 +0600 Subject: [PATCH] Continue updating validator rules for april2016 spec --- src/Language/Parser.php | 9 +- src/Type/Definition/NonNull.php | 2 +- src/Validator/DocumentValidator.php | 151 +---- src/Validator/Messages.php | 6 - src/Validator/Rules/NoUnusedVariables.php | 63 +- .../Rules/OverlappingFieldsCanBeMerged.php | 274 ++++++--- .../Rules/PossibleFragmentSpreads.php | 43 +- src/Validator/Rules/QueryComplexity.php | 7 +- src/Validator/Rules/QueryDepth.php | 4 +- src/Validator/Rules/UniqueArgumentNames.php | 43 ++ src/Validator/Rules/UniqueFragmentNames.php | 42 ++ src/Validator/Rules/UniqueInputFieldNames.php | 50 ++ src/Validator/Rules/UniqueOperationNames.php | 44 ++ src/Validator/Rules/UniqueVariableNames.php | 39 ++ .../Rules/VariablesInAllowedPosition.php | 72 ++- tests/Executor/AbstractTest.php | 28 +- tests/Validator/NoUnusedVariablesTest.php | 61 +- .../OverlappingFieldsCanBeMergedTest.php | 582 +++++++++++++++--- .../Validator/PossibleFragmentSpreadsTest.php | 67 ++ .../ProvidedNonNullArgumentsTest.php | 68 +- tests/Validator/QuerySecuritySchema.php | 4 +- tests/Validator/ScalarLeafsTest.php | 27 + tests/Validator/UniqueArgumentNamesTest.php | 186 ++++++ tests/Validator/UniqueFragmentNamesTest.php | 139 +++++ tests/Validator/UniqueInputFieldNamesTest.php | 104 ++++ tests/Validator/UniqueOperationNamesTest.php | 157 +++++ tests/Validator/UniqueVariableNamesTest.php | 47 ++ .../Validator/VariablesAreInputTypesTest.php | 7 + .../VariablesInAllowedPositionTest.php | 119 ++-- 29 files changed, 1937 insertions(+), 508 deletions(-) create mode 100644 src/Validator/Rules/UniqueArgumentNames.php create mode 100644 src/Validator/Rules/UniqueFragmentNames.php create mode 100644 src/Validator/Rules/UniqueInputFieldNames.php create mode 100644 src/Validator/Rules/UniqueOperationNames.php create mode 100644 src/Validator/Rules/UniqueVariableNames.php create mode 100644 tests/Validator/UniqueArgumentNamesTest.php create mode 100644 tests/Validator/UniqueFragmentNamesTest.php create mode 100644 tests/Validator/UniqueInputFieldNamesTest.php create mode 100644 tests/Validator/UniqueOperationNamesTest.php create mode 100644 tests/Validator/UniqueVariableNamesTest.php diff --git a/src/Language/Parser.php b/src/Language/Parser.php index 636f0fb..7e79dab 100644 --- a/src/Language/Parser.php +++ b/src/Language/Parser.php @@ -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([ diff --git a/src/Type/Definition/NonNull.php b/src/Type/Definition/NonNull.php index 5f9b5b3..abef493 100644 --- a/src/Type/Definition/NonNull.php +++ b/src/Type/Definition/NonNull.php @@ -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) diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index 2e1c828..6f55e0f 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -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; } } diff --git a/src/Validator/Messages.php b/src/Validator/Messages.php index 8ff38a4..54a5ba6 100644 --- a/src/Validator/Messages.php +++ b/src/Validator/Messages.php @@ -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); diff --git a/src/Validator/Rules/NoUnusedVariables.php b/src/Validator/Rules/NoUnusedVariables.php index 9a63a80..cf2fc87 100644 --- a/src/Validator/Rules/NoUnusedVariables.php +++ b/src/Validator/Rules/NoUnusedVariables.php @@ -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; } ]; } diff --git a/src/Validator/Rules/OverlappingFieldsCanBeMerged.php b/src/Validator/Rules/OverlappingFieldsCanBeMerged.php index f9194ac..2878f52 100644 --- a/src/Validator/Rules/OverlappingFieldsCanBeMerged.php +++ b/src/Validator/Rules/OverlappingFieldsCanBeMerged.php @@ -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,69 +107,114 @@ 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); - $name1 = $ast1->name->value; - $name2 = $ast2->name->value; - - if ($name1 !== $name2) { - return [ - [$responseName, "$name1 and $name2 are different fields"], - [$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 ($type1 && $type2 && !$this->sameType($type1, $type2)) { + // 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] + ]; + } + + $args1 = isset($ast1->arguments) ? $ast1->arguments : []; + $args2 = isset($ast2->arguments) ? $ast2->arguments : []; + + if (!$this->sameArguments($args1, $args2)) { + return [ + [$responseName, 'they have differing arguments'], + [$ast1], + [$ast2] + ]; + } + } + + + if ($type1 && $type2 && $this->doTypesConflict($type1, $type2)) { return [ - [$responseName, "they return differing types $type1 and $type2"], - [$ast1, $ast2] + [$responseName, "they return conflicting types $type1 and $type2"], + [$ast1], + [$ast2] ]; } - $args1 = isset($ast1->arguments) ? $ast1->arguments : []; - $args2 = isset($ast2->arguments) ? $ast2->arguments : []; + $subfieldMap = $this->getSubfieldMap($ast1, $type1, $ast2, $type2, $context); - if (!$this->sameArguments($args1, $args2)) { - return [ - [$responseName, 'they have differing arguments'], - [$ast1, $ast2] - ]; + if ($subfieldMap) { + $conflicts = $this->findConflicts($fieldsAreMutuallyExclusive, $subfieldMap, $context); + return $this->subfieldConflicts($conflicts, $responseName, $ast1, $ast2); } + return null; + } - $directives1 = isset($ast1->directives) ? $ast1->directives : []; - $directives2 = isset($ast2->directives) ? $ast2->directives : []; - - if (!$this->sameDirectives($directives1, $directives2)) { - return [ - [$responseName, 'they have differing directives'], - [$ast1, $ast2] - ]; - } - - $selectionSet1 = isset($ast1->selectionSet) ? $ast1->selectionSet : null; - $selectionSet2 = isset($ast2->selectionSet) ? $ast2->selectionSet : 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), @@ -159,23 +222,76 @@ class OverlappingFieldsCanBeMerged $visitedFragmentNames ); $subfieldMap = $this->collectFieldASTsAndDefs( - $context, - Type::getNamedType($type2), - $selectionSet2, - $visitedFragmentNames, - $subfieldMap + $context, + Type::getNamedType($type2), + $selectionSet2, + $visitedFragmentNames, + $subfieldMap ); - $conflicts = $this->findConflicts($subfieldMap, $context, $comparedSet); - - 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]) - ]; - } + return $subfieldMap; } } + private function subfieldConflicts( + array $conflicts, + $responseName, + Field $ast1, + Field $ast2 + ) + { + if (!empty($conflicts)) { + return [ + [ + $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; + } + /** * Given a selectionSet, adds all of the fields in that selection to * the passed in map of fields, and returns it at the end. @@ -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 $pairs1 * @param Array $pairs2 diff --git a/src/Validator/Rules/PossibleFragmentSpreads.php b/src/Validator/Rules/PossibleFragmentSpreads.php index 2062138..e51bcbb 100644 --- a/src/Validator/Rules/PossibleFragmentSpreads.php +++ b/src/Validator/Rules/PossibleFragmentSpreads.php @@ -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; } } diff --git a/src/Validator/Rules/QueryComplexity.php b/src/Validator/Rules/QueryComplexity.php index 8ce4afc..8edc6ae 100644 --- a/src/Validator/Rules/QueryComplexity.php +++ b/src/Validator/Rules/QueryComplexity.php @@ -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)) + ); } }, ], diff --git a/src/Validator/Rules/QueryDepth.php b/src/Validator/Rules/QueryDepth.php index 694262a..8097052 100644 --- a/src/Validator/Rules/QueryDepth.php +++ b/src/Validator/Rules/QueryDepth.php @@ -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)) + ); } }, ], diff --git a/src/Validator/Rules/UniqueArgumentNames.php b/src/Validator/Rules/UniqueArgumentNames.php new file mode 100644 index 0000000..84cb157 --- /dev/null +++ b/src/Validator/Rules/UniqueArgumentNames.php @@ -0,0 +1,43 @@ +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; + } + ]; + } +} diff --git a/src/Validator/Rules/UniqueFragmentNames.php b/src/Validator/Rules/UniqueFragmentNames.php new file mode 100644 index 0000000..6dd65eb --- /dev/null +++ b/src/Validator/Rules/UniqueFragmentNames.php @@ -0,0 +1,42 @@ +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; + } + ]; + } +} diff --git a/src/Validator/Rules/UniqueInputFieldNames.php b/src/Validator/Rules/UniqueInputFieldNames.php new file mode 100644 index 0000000..263790a --- /dev/null +++ b/src/Validator/Rules/UniqueInputFieldNames.php @@ -0,0 +1,50 @@ +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(); + } + ]; + } +} diff --git a/src/Validator/Rules/UniqueOperationNames.php b/src/Validator/Rules/UniqueOperationNames.php new file mode 100644 index 0000000..e8f8f8e --- /dev/null +++ b/src/Validator/Rules/UniqueOperationNames.php @@ -0,0 +1,44 @@ +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(); + } + ]; + } +} diff --git a/src/Validator/Rules/UniqueVariableNames.php b/src/Validator/Rules/UniqueVariableNames.php new file mode 100644 index 0000000..469813b --- /dev/null +++ b/src/Validator/Rules/UniqueVariableNames.php @@ -0,0 +1,39 @@ +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; + } + } + ]; + } +} diff --git a/src/Validator/Rules/VariablesInAllowedPosition.php b/src/Validator/Rules/VariablesInAllowedPosition.php index 9693161..cd568b4 100644 --- a/src/Validator/Rules/VariablesInAllowedPosition.php +++ b/src/Validator/Rules/VariablesInAllowedPosition.php @@ -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,42 +17,53 @@ 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::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(); + Node::OPERATION_DEFINITION => [ + 'enter' => function () { + $this->varDefMap = []; + }, + 'leave' => function(OperationDefinition $operation) use ($context) { + $usages = $context->getRecursiveVariableUsages($operation); - if ($varType && $inputType && - !$this->varTypeAllowedForType($this->effectiveType($varType, $varDef), $inputType) - ) { - $context->reportError(new Error( - Messages::badVarPosMessage($varName, $varType, $inputType), - [$variableAST] - )); + 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( + self::badVarPosMessage($varName, $varType, $type), + [$varDef, $node] + )); + } + } + } } + ], + Node::VARIABLE_DEFINITION => function (VariableDefinition $varDefAST) { + $this->varDefMap[$varDefAST->variable->name->value] = $varDefAST; } ]; } diff --git a/tests/Executor/AbstractTest.php b/tests/Executor/AbstractTest.php index 3540256..d3c86b9 100644 --- a/tests/Executor/AbstractTest.php +++ b/tests/Executor/AbstractTest.php @@ -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,17 +112,19 @@ class AbstractTest extends \PHPUnit_Framework_TestCase 'types' => [$dogType, $catType] ]); - $schema = new Schema(new ObjectType([ - 'name' => 'Query', - 'fields' => [ - 'pets' => [ - 'type' => Type::listOf($petType), - 'resolve' => function() { - return [ new Dog('Odie', true), new Cat('Garfield', false) ]; - } + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'pets' => [ + 'type' => Type::listOf($petType), + 'resolve' => function() { + return [ new Dog('Odie', true), new Cat('Garfield', false) ]; + } + ] ] - ] - ])); + ]) + ]); $query = '{ pets { diff --git a/tests/Validator/NoUnusedVariablesTest.php b/tests/Validator/NoUnusedVariablesTest.php index bd8f4e9..773eccf 100644 --- a/tests/Validator/NoUnusedVariablesTest.php +++ b/tests/Validator/NoUnusedVariablesTest.php @@ -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)] ); } diff --git a/tests/Validator/OverlappingFieldsCanBeMergedTest.php b/tests/Validator/OverlappingFieldsCanBeMergedTest.php index 1d779eb..6cfc9ed 100644 --- a/tests/Validator/OverlappingFieldsCanBeMergedTest.php +++ b/tests/Validator/OverlappingFieldsCanBeMergedTest.php @@ -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) + } + ... on Cat { + name + } } - ', [ - 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)] - ) - ]); + '); } + /** + * @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,41 +426,283 @@ 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, ' { - boxUnion { + someBox { ...on IntBox { scalar } - ...on StringBox { + ...on NonNullStringBox1 { scalar } } } - ', [ + ', [ 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 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, ' + { + someBox { + ... on IntBox { + scalar + } + ... on StringBox { + scalar + } + } + } + ', [ + FormattedError::create( + 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([ - 'name' => 'QueryRoot', - 'fields' => [ - 'boxUnion' => ['type' => $BoxUnion ], - 'connection' => ['type' => $Connection] - ] - ])); + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'QueryRoot', + 'fields' => [ + 'someBox' => ['type' => $SomeBox], + 'connection' => ['type' => $Connection] + ] + ]), + 'types' => [$IntBox, $StringBox, $NonNullStringBox1Impl, $NonNullStringBox2Impl] + ]); return $schema; } diff --git a/tests/Validator/PossibleFragmentSpreadsTest.php b/tests/Validator/PossibleFragmentSpreadsTest.php index fe64115..5e35cc5 100644 --- a/tests/Validator/PossibleFragmentSpreadsTest.php +++ b/tests/Validator/PossibleFragmentSpreadsTest.php @@ -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, ' diff --git a/tests/Validator/ProvidedNonNullArgumentsTest.php b/tests/Validator/ProvidedNonNullArgumentsTest.php index 2069428..dd5210d 100644 --- a/tests/Validator/ProvidedNonNullArgumentsTest.php +++ b/tests/Validator/ProvidedNonNullArgumentsTest.php @@ -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, ' diff --git a/tests/Validator/QuerySecuritySchema.php b/tests/Validator/QuerySecuritySchema.php index fc94224..aa95b88 100644 --- a/tests/Validator/QuerySecuritySchema.php +++ b/tests/Validator/QuerySecuritySchema.php @@ -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; } diff --git a/tests/Validator/ScalarLeafsTest.php b/tests/Validator/ScalarLeafsTest.php index 0b175a2..1ec748b 100644 --- a/tests/Validator/ScalarLeafsTest.php +++ b/tests/Validator/ScalarLeafsTest.php @@ -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, ' diff --git a/tests/Validator/UniqueArgumentNamesTest.php b/tests/Validator/UniqueArgumentNamesTest.php new file mode 100644 index 0000000..75fc607 --- /dev/null +++ b/tests/Validator/UniqueArgumentNamesTest.php @@ -0,0 +1,186 @@ +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)] + ); + } +} diff --git a/tests/Validator/UniqueFragmentNamesTest.php b/tests/Validator/UniqueFragmentNamesTest.php new file mode 100644 index 0000000..f465b12 --- /dev/null +++ b/tests/Validator/UniqueFragmentNamesTest.php @@ -0,0 +1,139 @@ +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)] + ); + } +} diff --git a/tests/Validator/UniqueInputFieldNamesTest.php b/tests/Validator/UniqueInputFieldNamesTest.php new file mode 100644 index 0000000..fad836d --- /dev/null +++ b/tests/Validator/UniqueInputFieldNamesTest.php @@ -0,0 +1,104 @@ +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)] + ); + } +} diff --git a/tests/Validator/UniqueOperationNamesTest.php b/tests/Validator/UniqueOperationNamesTest.php new file mode 100644 index 0000000..a1f18ef --- /dev/null +++ b/tests/Validator/UniqueOperationNamesTest.php @@ -0,0 +1,157 @@ +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)] + ); + } +} diff --git a/tests/Validator/UniqueVariableNamesTest.php b/tests/Validator/UniqueVariableNamesTest.php new file mode 100644 index 0000000..3a003f1 --- /dev/null +++ b/tests/Validator/UniqueVariableNamesTest.php @@ -0,0 +1,47 @@ +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)] + ); + } +} diff --git a/tests/Validator/VariablesAreInputTypesTest.php b/tests/Validator/VariablesAreInputTypesTest.php index b1ce0f2..407e5b4 100644 --- a/tests/Validator/VariablesAreInputTypesTest.php +++ b/tests/Validator/VariablesAreInputTypesTest.php @@ -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, ' diff --git a/tests/Validator/VariablesInAllowedPositionTest.php b/tests/Validator/VariablesInAllowedPositionTest.php index 3b43c0c..25dbbd1 100644 --- a/tests/Validator/VariablesInAllowedPositionTest.php +++ b/tests/Validator/VariablesInAllowedPositionTest.php @@ -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)] ) ]); }