|
19 | 19 | use Graphpinator\Typesystem\Exception\EnumItemInvalid; |
20 | 20 | use Graphpinator\Typesystem\Exception\FieldDirectiveNotCovariant; |
21 | 21 | use Graphpinator\Typesystem\Exception\FieldInvalidTypeUsage; |
| 22 | +use Graphpinator\Typesystem\Exception\FieldResolverNotIterable; |
| 23 | +use Graphpinator\Typesystem\Exception\FieldResolverNullabilityMismatch; |
| 24 | +use Graphpinator\Typesystem\Exception\FieldResolverVoidReturnType; |
22 | 25 | use Graphpinator\Typesystem\Exception\InputCycleDetected; |
23 | 26 | use Graphpinator\Typesystem\Exception\InputTypeMustDefineOneOreMoreFields; |
24 | 27 | use Graphpinator\Typesystem\Exception\InterfaceContractArgumentTypeMismatch; |
|
34 | 37 | use Graphpinator\Typesystem\Exception\UnionTypeMustDefineOneOrMoreTypes; |
35 | 38 | use Graphpinator\Typesystem\Exception\VarianceError; |
36 | 39 | use Graphpinator\Typesystem\Field\Field; |
| 40 | +use Graphpinator\Typesystem\Field\ResolvableField; |
37 | 41 | use Graphpinator\Typesystem\InputType; |
38 | 42 | use Graphpinator\Typesystem\InterfaceType; |
| 43 | +use Graphpinator\Typesystem\ListType; |
39 | 44 | use Graphpinator\Typesystem\Location\ArgumentDefinitionLocation; |
40 | 45 | use Graphpinator\Typesystem\Location\EnumItemLocation; |
41 | 46 | use Graphpinator\Typesystem\Location\EnumLocation; |
@@ -210,6 +215,14 @@ public function visitField(Field $field) : null |
210 | 215 | throw new FieldInvalidTypeUsage($field->getName(), $field->getType()->accept(new PrintNameVisitor())); |
211 | 216 | } |
212 | 217 |
|
| 218 | + foreach ($field->getArguments() as $argument) { |
| 219 | + $argument->accept($this); |
| 220 | + } |
| 221 | + |
| 222 | + if ($field instanceof ResolvableField) { |
| 223 | + self::validateFieldResolverFunction($field); |
| 224 | + } |
| 225 | + |
213 | 226 | foreach ($field->getDirectiveUsages() as $usage) { |
214 | 227 | $usage->accept($this); |
215 | 228 | $directive = $usage->getDirective(); |
@@ -440,6 +453,7 @@ private static function compareVariance(DirectiveUsageSet $biggerSet, DirectiveU |
440 | 453 |
|
441 | 454 | foreach ($biggerSet as $usage) { |
442 | 455 | $directive = $usage->getDirective(); |
| 456 | + \assert($directive instanceof FieldDefinitionLocation || $directive instanceof ArgumentDefinitionLocation); |
443 | 457 |
|
444 | 458 | if ($smallerSet->offsetExists($childIndex) && $directive instanceof ($smallerSet->offsetGet($childIndex)->getDirective())) { |
445 | 459 | $directive->validateVariance($usage->getArgumentValues(), $smallerSet->offsetGet($childIndex)->getArgumentValues()); |
@@ -533,4 +547,73 @@ private function validateContainer(Container $container) : void |
533 | 547 | $directive->accept($this); |
534 | 548 | } |
535 | 549 | } |
| 550 | + |
| 551 | + private static function validateFieldResolverFunction(ResolvableField $field) : void |
| 552 | + { |
| 553 | + $functionReturnType = (new \ReflectionFunction($field->getResolveFunction()))->getReturnType(); |
| 554 | + |
| 555 | + if (!$functionReturnType instanceof \ReflectionType) { |
| 556 | + return; // the return type is not present -> skip validation |
| 557 | + } |
| 558 | + |
| 559 | + if ($functionReturnType instanceof \ReflectionNamedType) { |
| 560 | + if ($functionReturnType->getName() === 'void') { |
| 561 | + throw new FieldResolverVoidReturnType($field->getName()); |
| 562 | + } |
| 563 | + |
| 564 | + if ($functionReturnType->getName() === 'never') { |
| 565 | + return; |
| 566 | + } |
| 567 | + } |
| 568 | + |
| 569 | + $fieldType = $field->getType(); |
| 570 | + $isFieldNotNull = $fieldType instanceof NotNullType; |
| 571 | + |
| 572 | + if ($functionReturnType->allowsNull() === $isFieldNotNull) { |
| 573 | + throw new FieldResolverNullabilityMismatch($field->getName()); |
| 574 | + } |
| 575 | + |
| 576 | + $shapingType = $fieldType->accept(new GetShapingTypeVisitor()); |
| 577 | + |
| 578 | + if ($shapingType instanceof ListType && !self::isReturnTypeIterable($functionReturnType)) { |
| 579 | + throw new FieldResolverNotIterable($field->getName()); |
| 580 | + } |
| 581 | + } |
| 582 | + |
| 583 | + private static function isReturnTypeIterable(\ReflectionType $type) : bool |
| 584 | + { |
| 585 | + if ($type instanceof \ReflectionNamedType) { |
| 586 | + $typeName = $type->getName(); |
| 587 | + |
| 588 | + return $typeName === 'array' |
| 589 | + || $typeName === 'iterable' |
| 590 | + || \is_a($typeName, \Traversable::class, true); |
| 591 | + } |
| 592 | + |
| 593 | + if ($type instanceof \ReflectionUnionType) { |
| 594 | + foreach ($type->getTypes() as $subType) { |
| 595 | + if ($subType instanceof \ReflectionNamedType && $subType->getName() === 'null') { |
| 596 | + continue; |
| 597 | + } |
| 598 | + |
| 599 | + if (!self::isReturnTypeIterable($subType)) { |
| 600 | + return false; |
| 601 | + } |
| 602 | + } |
| 603 | + |
| 604 | + return true; |
| 605 | + } |
| 606 | + |
| 607 | + if ($type instanceof \ReflectionIntersectionType) { |
| 608 | + foreach ($type->getTypes() as $subType) { |
| 609 | + if (self::isReturnTypeIterable($subType)) { |
| 610 | + return true; |
| 611 | + } |
| 612 | + } |
| 613 | + |
| 614 | + return false; |
| 615 | + } |
| 616 | + |
| 617 | + return false; |
| 618 | + } |
536 | 619 | } |
0 commit comments