diff --git a/src/GraphQL/CorrespondanceRule.php b/src/GraphQL/CorrespondanceRule.php index e3e8e9f..2eaae10 100644 --- a/src/GraphQL/CorrespondanceRule.php +++ b/src/GraphQL/CorrespondanceRule.php @@ -154,6 +154,7 @@ private function listFieldResolvedValueErrors( $objectType, $fieldName, $resolverClassType, + [], ); foreach ($errors as $error) { @@ -186,6 +187,7 @@ private function listFieldResolvedValueErrors( /** + * @param list $alreadyVisitedFields * @return array{list, list} */ private function listFieldResolvedValueTypes( @@ -194,15 +196,23 @@ private function listFieldResolvedValueTypes( string $objectType, string $fieldName, PHPStan\Type\ObjectType $resolverClassType, + array $alreadyVisitedFields, ): array { $errors = []; $types = []; + $parentTypes = $this->listObjectTypeResolvedValueTypes( + $scope, + $schemaServiceOraculum, + $objectType, + [...$alreadyVisitedFields, "$objectType.$fieldName"], + ); + if ($resolverClassType->getClassName() === Vojtechdobes\GraphQL\ArrayFieldResolver::class) { $offsetType = new PHPStan\Type\Constant\ConstantStringType($fieldName); - foreach ($this->listObjectTypeResolvedValueTypes($scope, $schemaServiceOraculum, $objectType) as $parentType) { + foreach ($parentTypes as $parentType) { if ($parentType->isOffsetAccessible()->yes() === false) { $errors[] = sprintf( "Resolver %s of field %s expects parent to have array access, but parent is resolved to %s", @@ -225,7 +235,7 @@ private function listFieldResolvedValueTypes( } elseif ($resolverClassType->getClassName() === Vojtechdobes\GraphQL\GetterFieldResolver::class) { $methodName = 'get' . ucfirst($fieldName); - foreach ($this->listObjectTypeResolvedValueTypes($scope, $schemaServiceOraculum, $objectType) as $parentType) { + foreach ($parentTypes as $parentType) { if ($parentType->isObject()->yes() === false) { $errors[] = sprintf( "Resolver %s of field %s expects parent to be an object, but parent is resolved to %s", @@ -250,7 +260,7 @@ private function listFieldResolvedValueTypes( } } } elseif ($resolverClassType->getClassName() === Vojtechdobes\GraphQL\PropertyFieldResolver::class) { - foreach ($this->listObjectTypeResolvedValueTypes($scope, $schemaServiceOraculum, $objectType) as $parentType) { + foreach ($parentTypes as $parentType) { if ($parentType->isObject()->yes() === false) { $errors[] = sprintf( "Resolver %s of field %s expects parent to be an object, but parent is resolved to %s", @@ -274,7 +284,7 @@ private function listFieldResolvedValueTypes( } else { $expectedParentType = $resolverClassType->getTemplateType(Vojtechdobes\GraphQL\FieldResolver::class, 'TObjectValue'); - foreach ($this->listObjectTypeResolvedValueTypes($scope, $schemaServiceOraculum, $objectType) as $parentType) { + foreach ($parentTypes as $parentType) { if ($expectedParentType->isSuperTypeOf($parentType)->yes() === false) { $errors[] = sprintf( "Resolver %s of field %s expects parent to be %s, but parent is resolved to %s", @@ -303,23 +313,36 @@ private function listFieldResolvedValueTypes( /** + * @param list $alreadyVisitedFields * @return list */ private function listObjectTypeResolvedValueTypes( PHPStan\Analyser\Scope $scope, SchemaServiceOraculum $schemaServiceOraculum, string $objectType, + array $alreadyVisitedFields, ): array { $result = []; + if ($objectType === $schemaServiceOraculum->getRootOperationType(Vojtechdobes\GraphQL\OperationType::Query)) { + return [new PHPStan\Type\NullType()]; + } + foreach ($schemaServiceOraculum->listFieldsResolvedToObjectType($objectType) as [$parentObjectType, $parentFieldName]) { + $parentField = "{$parentObjectType}.{$parentFieldName}"; + + if (in_array($parentField, $alreadyVisitedFields, true)) { + continue; + } + [$parentTypes] = $this->listFieldResolvedValueTypes( $scope, $schemaServiceOraculum, $parentObjectType, $parentFieldName, $schemaServiceOraculum->getFieldResolverType($parentObjectType, $parentFieldName), + [...$alreadyVisitedFields, $parentField], ); foreach ($parentTypes as $parentType) { diff --git a/tests-shared/AbstractCorrespondanceRuleTest.php b/tests-shared/AbstractCorrespondanceRuleTest.php index ba4f272..5ee5c6a 100644 --- a/tests-shared/AbstractCorrespondanceRuleTest.php +++ b/tests-shared/AbstractCorrespondanceRuleTest.php @@ -40,6 +40,10 @@ final public function testRule(): void "Arguments array{arg1: string|null} of field Query.invalidArgumentsMismatch aren't contravariant with arguments array{} of resolver Vojtechdobes\TestsShared\Resolvers\QueryInvalidArgumentsMismatchFieldResolver", -1, ], + [ + "Resolver Vojtechdobes\GraphQL\PropertyFieldResolver of field Query.rootFieldWithParentBasedResolver expects parent to be an object, but parent is resolved to null", + -1, + ], [ "Resolver Vojtechdobes\TestsShared\Resolvers\PersonParentTypeNameFieldResolver of field PersonParentType.name expects parent to be Vojtechdobes\TestsShared\Resolvers\Person, but parent is resolved to array{}", -1, diff --git a/tests-shared/Resolvers/QueryValidSelfReferenceFieldResolver.php b/tests-shared/Resolvers/QueryValidSelfReferenceFieldResolver.php new file mode 100644 index 0000000..5ed4975 --- /dev/null +++ b/tests-shared/Resolvers/QueryValidSelfReferenceFieldResolver.php @@ -0,0 +1,19 @@ + + */ +final class QueryValidSelfReferenceFieldResolver implements Vojtechdobes\GraphQL\FieldResolver +{ + + public function resolveField(mixed $objectValue, Vojtechdobes\GraphQL\FieldSelection $field): mixed + { + return new SelfReference(internalSelfReference: null); + } + +} diff --git a/tests-shared/Resolvers/SelfReference.php b/tests-shared/Resolvers/SelfReference.php new file mode 100644 index 0000000..9c1013b --- /dev/null +++ b/tests-shared/Resolvers/SelfReference.php @@ -0,0 +1,13 @@ + new Vojtechdobes\TestsShared\Resolvers\QueryProviderOfValidEntityParentTypePersonFieldResolver(), 'Query.providerOfValidEntityParentTypeThing' => new Vojtechdobes\TestsShared\Resolvers\QueryProviderOfValidEntityParentTypeThingFieldResolver(), + 'Query.rootFieldWithParentBasedResolver' => new Vojtechdobes\GraphQL\PropertyFieldResolver(), + 'PersonParentType.name' => new Vojtechdobes\TestsShared\Resolvers\PersonParentTypeNameFieldResolver(), 'EntityParentType.name' => new Vojtechdobes\TestsShared\Resolvers\EntityParentTypeNameFieldResolver(), + + 'Query.validSelfReference' => new Vojtechdobes\TestsShared\Resolvers\QueryValidSelfReferenceFieldResolver(), + 'SelfReference.internalSelfReference' => new Vojtechdobes\GraphQL\PropertyFieldResolver(), ]); }