public function __invoke(ValidationContext $context) { return [Node::INLINE_FRAGMENT => function (InlineFragment $node) use($context) { $fragType = Type::getNamedType($context->getType()); $parentType = $context->getParentType(); if ($fragType && $parentType && !$this->doTypesOverlap($fragType, $parentType)) { return new Error(self::typeIncompatibleAnonSpreadMessage($parentType, $fragType), [$node]); } }, Node::FRAGMENT_SPREAD => function (FragmentSpread $node) use($context) { $fragName = $node->name->value; $fragType = Type::getNamedType($this->getFragmentType($context, $fragName)); $parentType = $context->getParentType(); if ($fragType && $parentType && !$this->doTypesOverlap($fragType, $parentType)) { return new Error(self::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType), [$node]); } }]; }
private function getSubfieldMap(FieldNode $ast1, $type1, FieldNode $ast2, $type2, ValidationContext $context) { $selectionSet1 = $ast1->selectionSet; $selectionSet2 = $ast2->selectionSet; if ($selectionSet1 && $selectionSet2) { $visitedFragmentNames = new \ArrayObject(); $subfieldMap = $this->collectFieldNodesAndDefs($context, Type::getNamedType($type1), $selectionSet1, $visitedFragmentNames); $subfieldMap = $this->collectFieldNodesAndDefs($context, Type::getNamedType($type2), $selectionSet2, $visitedFragmentNames, $subfieldMap); return $subfieldMap; } }
/** * @it maintains type info during edit */ public function testMaintainsTypeInfoDuringEdit() { $visited = []; $typeInfo = new TypeInfo(TestCase::getDefaultSchema()); $ast = Parser::parse('{ human(id: 4) { name, pets }, alien }'); $editedAst = Visitor::visit($ast, Visitor::visitWithTypeInfo($typeInfo, ['enter' => function ($node) use($typeInfo, &$visited) { $parentType = $typeInfo->getParentType(); $type = $typeInfo->getType(); $inputType = $typeInfo->getInputType(); $visited[] = ['enter', $node->kind, $node->kind === 'Name' ? $node->value : null, $parentType ? (string) $parentType : null, $type ? (string) $type : null, $inputType ? (string) $inputType : null]; // Make a query valid by adding missing selection sets. if ($node->kind === 'Field' && !$node->selectionSet && Type::isCompositeType(Type::getNamedType($type))) { return new FieldNode(['alias' => $node->alias, 'name' => $node->name, 'arguments' => $node->arguments, 'directives' => $node->directives, 'selectionSet' => new SelectionSetNode(['kind' => 'SelectionSet', 'selections' => [new FieldNode(['name' => new NameNode(['value' => '__typename'])])]])]); } }, 'leave' => function ($node) use($typeInfo, &$visited) { $parentType = $typeInfo->getParentType(); $type = $typeInfo->getType(); $inputType = $typeInfo->getInputType(); $visited[] = ['leave', $node->kind, $node->kind === 'Name' ? $node->value : null, $parentType ? (string) $parentType : null, $type ? (string) $type : null, $inputType ? (string) $inputType : null]; }])); $this->assertEquals(Printer::doPrint(Parser::parse('{ human(id: 4) { name, pets }, alien }')), Printer::doPrint($ast)); $this->assertEquals(Printer::doPrint(Parser::parse('{ human(id: 4) { name, pets { __typename } }, alien { __typename } }')), Printer::doPrint($editedAst)); $this->assertEquals([['enter', 'Document', null, null, null, null], ['enter', 'OperationDefinition', null, null, 'QueryRoot', null], ['enter', 'SelectionSet', null, 'QueryRoot', 'QueryRoot', null], ['enter', 'Field', null, 'QueryRoot', 'Human', null], ['enter', 'Name', 'human', 'QueryRoot', 'Human', null], ['leave', 'Name', 'human', 'QueryRoot', 'Human', null], ['enter', 'Argument', null, 'QueryRoot', 'Human', 'ID'], ['enter', 'Name', 'id', 'QueryRoot', 'Human', 'ID'], ['leave', 'Name', 'id', 'QueryRoot', 'Human', 'ID'], ['enter', 'IntValue', null, 'QueryRoot', 'Human', 'ID'], ['leave', 'IntValue', null, 'QueryRoot', 'Human', 'ID'], ['leave', 'Argument', null, 'QueryRoot', 'Human', 'ID'], ['enter', 'SelectionSet', null, 'Human', 'Human', null], ['enter', 'Field', null, 'Human', 'String', null], ['enter', 'Name', 'name', 'Human', 'String', null], ['leave', 'Name', 'name', 'Human', 'String', null], ['leave', 'Field', null, 'Human', 'String', null], ['enter', 'Field', null, 'Human', '[Pet]', null], ['enter', 'Name', 'pets', 'Human', '[Pet]', null], ['leave', 'Name', 'pets', 'Human', '[Pet]', null], ['enter', 'SelectionSet', null, 'Pet', '[Pet]', null], ['enter', 'Field', null, 'Pet', 'String!', null], ['enter', 'Name', '__typename', 'Pet', 'String!', null], ['leave', 'Name', '__typename', 'Pet', 'String!', null], ['leave', 'Field', null, 'Pet', 'String!', null], ['leave', 'SelectionSet', null, 'Pet', '[Pet]', null], ['leave', 'Field', null, 'Human', '[Pet]', null], ['leave', 'SelectionSet', null, 'Human', 'Human', null], ['leave', 'Field', null, 'QueryRoot', 'Human', null], ['enter', 'Field', null, 'QueryRoot', 'Alien', null], ['enter', 'Name', 'alien', 'QueryRoot', 'Alien', null], ['leave', 'Name', 'alien', 'QueryRoot', 'Alien', null], ['enter', 'SelectionSet', null, 'Alien', 'Alien', null], ['enter', 'Field', null, 'Alien', 'String!', null], ['enter', 'Name', '__typename', 'Alien', 'String!', null], ['leave', 'Name', '__typename', 'Alien', 'String!', null], ['leave', 'Field', null, 'Alien', 'String!', null], ['leave', 'SelectionSet', null, 'Alien', 'Alien', null], ['leave', 'Field', null, 'QueryRoot', 'Alien', null], ['leave', 'SelectionSet', null, 'QueryRoot', 'QueryRoot', null], ['leave', 'OperationDefinition', null, null, 'QueryRoot', null], ['leave', 'Document', null, null, null, null]], $visited); }
/** * @param Node $node */ function enter(Node $node) { $schema = $this->schema; switch ($node->kind) { case Node::SELECTION_SET: $namedType = Type::getNamedType($this->getType()); $compositeType = null; if (Type::isCompositeType($namedType)) { // isCompositeType is a type refining predicate, so this is safe. $compositeType = $namedType; } $this->parentTypeStack[] = $compositeType; // push break; case Node::FIELD: $parentType = $this->getParentType(); $fieldDef = null; if ($parentType) { $fieldDef = self::getFieldDefinition($schema, $parentType, $node); } $this->fieldDefStack[] = $fieldDef; // push $this->typeStack[] = $fieldDef ? $fieldDef->getType() : null; // push break; case Node::DIRECTIVE: $this->directive = $schema->getDirective($node->name->value); break; case Node::OPERATION_DEFINITION: $type = null; if ($node->operation === 'query') { $type = $schema->getQueryType(); } else { if ($node->operation === 'mutation') { $type = $schema->getMutationType(); } else { if ($node->operation === 'subscription') { $type = $schema->getSubscriptionType(); } } } $this->typeStack[] = $type; // push break; case Node::INLINE_FRAGMENT: case Node::FRAGMENT_DEFINITION: $typeConditionAST = $node->typeCondition; $outputType = $typeConditionAST ? self::typeFromAST($schema, $typeConditionAST) : $this->getType(); $this->typeStack[] = $outputType; // push break; case Node::VARIABLE_DEFINITION: $inputType = self::typeFromAST($schema, $node->type); $this->inputTypeStack[] = $inputType; // push break; case Node::ARGUMENT: $fieldOrDirective = $this->getDirective() ?: $this->getFieldDef(); $argDef = $argType = null; if ($fieldOrDirective) { $argDef = Utils::find($fieldOrDirective->args, function ($arg) use($node) { return $arg->name === $node->name->value; }); if ($argDef) { $argType = $argDef->getType(); } } $this->argument = $argDef; $this->inputTypeStack[] = $argType; // push break; case Node::LST: $listType = Type::getNullableType($this->getInputType()); $this->inputTypeStack[] = $listType instanceof ListOfType ? $listType->getWrappedType() : null; // push break; case Node::OBJECT_FIELD: $objectType = Type::getNamedType($this->getInputType()); $fieldType = null; if ($objectType instanceof InputObjectType) { $tmp = $objectType->getFields(); $inputField = isset($tmp[$node->name->value]) ? $tmp[$node->name->value] : null; $fieldType = $inputField ? $inputField->getType() : null; } $this->inputTypeStack[] = $fieldType; break; } }
/** * @param ValidationContext $context * @param PairSet $comparedSet * @param $responseName * @param [Field, GraphQLFieldDefinition] $pair1 * @param [Field, GraphQLFieldDefinition] $pair2 * @return array|null */ private function findConflict($responseName, array $pair1, array $pair2, ValidationContext $context, PairSet $comparedSet) { list($ast1, $def1) = $pair1; list($ast2, $def2) = $pair2; if ($ast1 === $ast2 || $comparedSet->has($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]]; } $type1 = isset($def1) ? $def1->getType() : null; $type2 = isset($def2) ? $def2->getType() : null; if ($type1 && $type2 && !$this->sameType($type1, $type2)) { return [[$responseName, "they return differing types {$type1} and {$type2}"], [$ast1, $ast2]]; } $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]]; } $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; if ($selectionSet1 && $selectionSet2) { $visitedFragmentNames = new \ArrayObject(); $subfieldMap = $this->collectFieldASTsAndDefs($context, Type::getNamedType($type1), $selectionSet1, $visitedFragmentNames); $subfieldMap = $this->collectFieldASTsAndDefs($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])]; } } }