From a0b21559d49ab4e1fa0237e67640c1e5354fa381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Dobe=C5=A1?= Date: Thu, 5 Jun 2025 12:38:01 +0200 Subject: [PATCH 1/2] Report parent-based resolvers on root fields --- src/GraphQL/CorrespondanceRule.php | 4 ++++ tests-shared/AbstractCorrespondanceRuleTest.php | 4 ++++ tests-shared/schema.graphqls | 2 ++ tests/CustomAdapter.php | 2 ++ 4 files changed, 12 insertions(+) diff --git a/src/GraphQL/CorrespondanceRule.php b/src/GraphQL/CorrespondanceRule.php index e3e8e9f..d04ce3d 100644 --- a/src/GraphQL/CorrespondanceRule.php +++ b/src/GraphQL/CorrespondanceRule.php @@ -313,6 +313,10 @@ private function listObjectTypeResolvedValueTypes( { $result = []; + if ($objectType === $schemaServiceOraculum->getRootOperationType(Vojtechdobes\GraphQL\OperationType::Query)) { + return [new PHPStan\Type\NullType()]; + } + foreach ($schemaServiceOraculum->listFieldsResolvedToObjectType($objectType) as [$parentObjectType, $parentFieldName]) { [$parentTypes] = $this->listFieldResolvedValueTypes( $scope, 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/schema.graphqls b/tests-shared/schema.graphqls index 7d681dd..812bad4 100644 --- a/tests-shared/schema.graphqls +++ b/tests-shared/schema.graphqls @@ -18,6 +18,8 @@ type Query { providerOfValidEntityParentTypeEntity: EntityParentType! providerOfValidEntityParentTypePerson: EntityParentType! providerOfValidEntityParentTypeThing: EntityParentType! + + rootFieldWithParentBasedResolver: String # should be eg. PropertyFieldResolver } type ArrayType { diff --git a/tests/CustomAdapter.php b/tests/CustomAdapter.php index f741dd2..6c0ccba 100644 --- a/tests/CustomAdapter.php +++ b/tests/CustomAdapter.php @@ -52,6 +52,8 @@ public function getFieldResolverProvider(string $schemaName): Vojtechdobes\Graph 'Query.providerOfValidEntityParentTypePerson' => 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(), ]); From 06f817f9028cd73f3f5be96a0b13e897b84c305d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Dobe=C5=A1?= Date: Thu, 5 Jun 2025 12:45:37 +0200 Subject: [PATCH 2/2] Don't loop infinitely on self-referencing types --- src/GraphQL/CorrespondanceRule.php | 27 ++++++++++++++++--- .../QueryValidSelfReferenceFieldResolver.php | 19 +++++++++++++ tests-shared/Resolvers/SelfReference.php | 13 +++++++++ tests-shared/schema.graphqls | 6 +++++ tests/CustomAdapter.php | 3 +++ 5 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 tests-shared/Resolvers/QueryValidSelfReferenceFieldResolver.php create mode 100644 tests-shared/Resolvers/SelfReference.php diff --git a/src/GraphQL/CorrespondanceRule.php b/src/GraphQL/CorrespondanceRule.php index d04ce3d..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,12 +313,14 @@ private function listFieldResolvedValueTypes( /** + * @param list $alreadyVisitedFields * @return list */ private function listObjectTypeResolvedValueTypes( PHPStan\Analyser\Scope $scope, SchemaServiceOraculum $schemaServiceOraculum, string $objectType, + array $alreadyVisitedFields, ): array { $result = []; @@ -318,12 +330,19 @@ private function listObjectTypeResolvedValueTypes( } 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/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\PersonParentTypeNameFieldResolver(), 'EntityParentType.name' => new Vojtechdobes\TestsShared\Resolvers\EntityParentTypeNameFieldResolver(), + + 'Query.validSelfReference' => new Vojtechdobes\TestsShared\Resolvers\QueryValidSelfReferenceFieldResolver(), + 'SelfReference.internalSelfReference' => new Vojtechdobes\GraphQL\PropertyFieldResolver(), ]); }