diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e55e6edd..7ae93bd07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ You can find and compare releases at the [GitHub release page](https://github.co - Use native PHP types for properties of `Type` and its subclasses - Throw `SerializationError` over client safe `Error` when failing to serialize leaf types - Move debug entries in errors under `extensions` key -- Use native PHP types for `Schema` and `SchemaConfig` +- Use native PHP types wherever possible - Always throw `RequestError` with useful message when clients provide an invalid JSON body - Move class `BlockString` from namespace `GraphQL\Utils` to `GraphQL\Language` - Return string-keyed arrays from `GraphQL::getStandardDirectives()`, `GraphQL::getStandardTypes()` and `GraphQL::getStandardValidationRules()` @@ -32,6 +32,11 @@ You can find and compare releases at the [GitHub release page](https://github.co - Always convert recursively when calling `Node::toArray()` - Make `Directive::$config['args']` use the same definition style as `FieldDefinition::$config['args']` - Rename `FieldArgument` to `Argument` +- Make errors when parsing scalar literals more precise +- Change expected `QueryPlan` options from `['group-implementor-fields']` to `['groupImplementorFields' => true]` in `ResolveInfo::lookAhead()` +- Always convert promises through `PromiseAdapter::convertThenable()` before calling `->then()` on them +- Use `JSON_THROW_ON_ERROR` in `json_encode()` +- Validate some internal invariants through `assert()` ### Added @@ -97,6 +102,7 @@ You can find and compare releases at the [GitHub release page](https://github.co - Remove `ListOfType::$ofType`, `ListOfType::getOfType()` and `NonNull::getOfType()` - Remove option `commentDescriptions` from `BuildSchema::buildAST()`, `BuildSchema::build()` and `Printer::doPrint()` - Remove parameter `$options` from `ASTDefinitionBuilder` +- Remove `FieldDefinition::create()` in favor of `new FieldDefinition()` ## 14.11.3 diff --git a/benchmarks/Utils/SchemaGenerator.php b/benchmarks/Utils/SchemaGenerator.php index a9ef523b0..629d9d85c 100644 --- a/benchmarks/Utils/SchemaGenerator.php +++ b/benchmarks/Utils/SchemaGenerator.php @@ -89,7 +89,7 @@ protected function createType(int $nestingLevel, ?string $typeName = null): Obje } /** - * @return array{0: ObjectType, 1: string} + * @return array{0: Type, 1: string} */ protected function getFieldTypeAndName(int $nestingLevel, int $fieldIndex): array { diff --git a/composer.json b/composer.json index 3ef64bc25..997150c7b 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,8 @@ "psr/http-message": "^1", "react/promise": "^2", "symfony/polyfill-php81": "^1.23", - "symfony/var-exporter": "^5.3" + "symfony/var-exporter": "^5.3", + "thecodingmachine/safe": "^1.3" }, "suggest": { "psr/http-message": "To use standard GraphQL server", diff --git a/docs/class-reference.md b/docs/class-reference.md index 779a7b608..99ee51d0b 100644 --- a/docs/class-reference.md +++ b/docs/class-reference.md @@ -2346,7 +2346,7 @@ static function toArray(GraphQL\Language\AST\Node $node): array * | null | NullValue | * * @param Type|mixed|null $value - * @param ScalarType|EnumType|InputObjectType|ListOfType|NonNull $type + * @param ScalarType|EnumType|InputObjectType|ListOfType|NonNull $type * * @return ObjectValueNode|ListValueNode|BooleanValueNode|IntValueNode|FloatValueNode|EnumValueNode|StringValueNode|NullValueNode|null * diff --git a/examples/00-hello-world/graphql.php b/examples/00-hello-world/graphql.php index 89bcd82f4..8012d46f9 100644 --- a/examples/00-hello-world/graphql.php +++ b/examples/00-hello-world/graphql.php @@ -58,6 +58,10 @@ ]); $rawInput = file_get_contents('php://input'); + if (false === $rawInput) { + throw new RuntimeException('Failed to get php://input'); + } + $input = json_decode($rawInput, true); $query = $input['query']; $variableValues = $input['variables'] ?? null; diff --git a/examples/01-blog/Blog/Type/NodeType.php b/examples/01-blog/Blog/Type/NodeType.php index f1c41854b..b15716ef3 100644 --- a/examples/01-blog/Blog/Type/NodeType.php +++ b/examples/01-blog/Blog/Type/NodeType.php @@ -5,13 +5,13 @@ namespace GraphQL\Examples\Blog\Type; use Exception; -use function get_class; use GraphQL\Examples\Blog\Data\Image; use GraphQL\Examples\Blog\Data\Story; use GraphQL\Examples\Blog\Data\User; use GraphQL\Examples\Blog\Types; use GraphQL\Type\Definition\InterfaceType; -use GraphQL\Type\Definition\Type; +use GraphQL\Type\Definition\ObjectType; +use GraphQL\Utils\Utils; class NodeType extends InterfaceType { @@ -26,7 +26,12 @@ public function __construct() ]); } - public function resolveNodeType(object $object): Type + /** + * @param mixed $object + * + * @return callable(): ObjectType + */ + public function resolveNodeType($object) { if ($object instanceof User) { return Types::user(); @@ -40,6 +45,6 @@ public function resolveNodeType(object $object): Type return Types::story(); } - throw new Exception('Unknown type: ' . get_class($object)); + throw new Exception('Unknown type: ' . Utils::printSafe($object)); } } diff --git a/examples/01-blog/Blog/Type/Scalar/UrlType.php b/examples/01-blog/Blog/Type/Scalar/UrlType.php index 47bb4fe21..e3f7fa716 100644 --- a/examples/01-blog/Blog/Type/Scalar/UrlType.php +++ b/examples/01-blog/Blog/Type/Scalar/UrlType.php @@ -57,6 +57,6 @@ public function parseLiteral(Node $valueNode, ?array $variables = null): string private function isUrl($value): bool { return is_string($value) - && filter_var($value, FILTER_VALIDATE_URL); + && false !== filter_var($value, FILTER_VALIDATE_URL); } } diff --git a/examples/01-blog/Blog/Types.php b/examples/01-blog/Blog/Types.php index eed37fcf3..e356eabd6 100644 --- a/examples/01-blog/Blog/Types.php +++ b/examples/01-blog/Blog/Types.php @@ -4,7 +4,6 @@ namespace GraphQL\Examples\Blog; -use function class_exists; use Closure; use function count; use Exception; @@ -94,6 +93,8 @@ public static function url(): callable } /** + * @param class-string $classname + * * @return Closure(): Type */ private static function get(string $classname): Closure @@ -101,28 +102,20 @@ private static function get(string $classname): Closure return static fn () => self::byClassName($classname); } + /** + * @param class-string $classname + */ private static function byClassName(string $classname): Type { $parts = explode('\\', $classname); $cacheName = strtolower(preg_replace('~Type$~', '', $parts[count($parts) - 1])); - $type = null; if (! isset(self::$types[$cacheName])) { - if (class_exists($classname)) { - $type = new $classname(); - } - - self::$types[$cacheName] = $type; + return self::$types[$cacheName] = new $classname(); } - $type = self::$types[$cacheName]; - - if (! $type) { - throw new Exception('Unknown graphql type: ' . $classname); - } - - return $type; + return self::$types[$cacheName]; } public static function byTypeName(string $shortName): Type diff --git a/examples/02-schema-definition-language/graphql.php b/examples/02-schema-definition-language/graphql.php index 10830e240..fe27e0ce2 100644 --- a/examples/02-schema-definition-language/graphql.php +++ b/examples/02-schema-definition-language/graphql.php @@ -33,6 +33,10 @@ ]; $rawInput = file_get_contents('php://input'); + if (false === $rawInput) { + throw new RuntimeException('Failed to get php://input'); + } + $input = json_decode($rawInput, true); $query = $input['query']; $variableValues = $input['variables'] ?? null; diff --git a/generate-class-reference.php b/generate-class-reference.php index 390db6c84..121f88663 100644 --- a/generate-class-reference.php +++ b/generate-class-reference.php @@ -174,12 +174,9 @@ function renderProp(ReflectionProperty $prop): string return unpadDocblock($prop->getDocComment()) . "\n" . $signature; } -/** - * @param string|false $docBlock - */ -function unwrapDocblock($docBlock): string +function unwrapDocblock(string $docBlock): string { - if (! $docBlock) { + if ('' === $docBlock) { return ''; } @@ -196,7 +193,7 @@ function unwrapDocblock($docBlock): string */ function unpadDocblock($docBlock): string { - if (! $docBlock) { + if (false === $docBlock) { return ''; } @@ -215,7 +212,7 @@ function unpadDocblock($docBlock): string function isApi(Reflector $reflector): bool { $comment = $reflector->getDocComment(); - if (! $comment) { + if (false === $comment) { return false; } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ad0b5f557..c7c327430 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -45,11 +45,6 @@ parameters: count: 1 path: examples/01-blog/Blog/Types.php - - - message: "#^Strict comparison using \\=\\=\\= between non\\-empty\\-string and null will always evaluate to false\\.$#" - count: 1 - path: src/Error/Error.php - - message: "#^SplObjectStorage\\, ArrayObject\\\\>\\>\\> does not accept SplObjectStorage\\, ArrayObject\\\\>\\>\\|SplObjectStorage\\\\.$#" count: 1 @@ -76,8 +71,8 @@ parameters: path: src/Language/AST/Node.php - - message: "#^Array \\(array\\\\|T of GraphQL\\\\Language\\\\AST\\\\Node\\>\\) does not accept GraphQL\\\\Language\\\\AST\\\\Node\\.$#" - count: 2 + message: "#^Parameter \\#4 \\$replacement of function array_splice expects array\\|string, array\\\\|T of GraphQL\\\\Language\\\\AST\\\\Node\\|null given\\.$#" + count: 1 path: src/Language/AST/NodeList.php - @@ -110,6 +105,16 @@ parameters: count: 1 path: src/Language/Visitor.php + - + message: "#^Parameter \\#1 \\$config of class GraphQL\\\\Type\\\\Definition\\\\Argument constructor expects array\\{name\\: string, type\\: \\(callable\\(\\)\\: GraphQL\\\\Type\\\\Definition\\\\InputType&GraphQL\\\\Type\\\\Definition\\\\Type\\)\\|\\(GraphQL\\\\Type\\\\Definition\\\\InputType&GraphQL\\\\Type\\\\Definition\\\\Type\\), defaultValue\\?\\: mixed, description\\?\\: string\\|null, astNode\\?\\: GraphQL\\\\Language\\\\AST\\\\InputValueDefinitionNode\\|null\\}, non\\-empty\\-array given\\.$#" + count: 1 + path: src/Type/Definition/Argument.php + + - + message: "#^Parameter \\#1 \\$config of class GraphQL\\\\Type\\\\Definition\\\\EnumValueDefinition constructor expects array\\{name\\: string, value\\?\\: mixed, deprecationReason\\?\\: string\\|null, description\\?\\: string\\|null, astNode\\?\\: GraphQL\\\\Language\\\\AST\\\\EnumValueDefinitionNode\\|null\\}, array given\\.$#" + count: 1 + path: src/Type/Definition/EnumType.php + - message: "#^Method GraphQL\\\\Type\\\\Definition\\\\FieldDefinition\\:\\:getType\\(\\) should return GraphQL\\\\Type\\\\Definition\\\\OutputType&GraphQL\\\\Type\\\\Definition\\\\Type but returns GraphQL\\\\Type\\\\Definition\\\\Type\\.$#" count: 1 @@ -120,6 +125,11 @@ parameters: count: 1 path: src/Type/Definition/FieldDefinition.php + - + message: "#^Method GraphQL\\\\Type\\\\Definition\\\\NonNull\\:\\:getWrappedType\\(\\) should return GraphQL\\\\Type\\\\Definition\\\\NullableType&GraphQL\\\\Type\\\\Definition\\\\Type but returns GraphQL\\\\Type\\\\Definition\\\\Type\\.$#" + count: 1 + path: src/Type/Definition/NonNull.php + - message: "#^Array \\(array\\\\) does not accept GraphQL\\\\Type\\\\Definition\\\\Type\\.$#" count: 1 @@ -161,19 +171,14 @@ parameters: path: src/Validator/Rules/KnownDirectives.php - - message: "#^SplObjectStorage\\\\>, array\\\\}\\> does not accept array\\\\.$#" + message: "#^Method GraphQL\\\\Validator\\\\Rules\\\\OverlappingFieldsCanBeMerged\\:\\:getFieldsAndFragmentNames\\(\\) should return array\\{array\\\\>, array\\\\} but returns array\\{mixed, array\\\\}\\.$#" count: 1 path: src/Validator/Rules/OverlappingFieldsCanBeMerged.php - - message: "#^Call to static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertArrayHasKey\\(\\) with int\\|string and GraphQL\\\\Language\\\\AST\\\\NodeList will always evaluate to false\\.$#" - count: 1 - path: tests/Language/VisitorTest.php - - - - message: "#^Instanceof between mixed and GraphQL\\\\Language\\\\AST\\\\NodeList will always evaluate to false\\.$#" + message: "#^SplObjectStorage\\\\>, array\\\\}\\> does not accept array\\\\.$#" count: 1 - path: tests/Language/VisitorTest.php + path: src/Validator/Rules/OverlappingFieldsCanBeMerged.php - message: "#^Method GraphQL\\\\Tests\\\\Language\\\\VisitorTest\\:\\:getNodeByPath\\(\\) return type with generic class GraphQL\\\\Language\\\\AST\\\\NodeList does not specify its types\\: T$#" diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 4588c7be7..0f5faf216 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,9 +1,6 @@ parameters: # TODO increase to max - level: 6 - - # TODO revert this setting once we arrived at level: max - reportUnmatchedIgnoredErrors: false + level: 7 paths: - benchmarks @@ -22,7 +19,6 @@ parameters: - "~Variable method call on GraphQL\\\\Language\\\\Parser\\.~" # We utilize lazy initialization of non-nullable properties - - "~Property .+? in isset\\(\\) is not nullable~" - "~Property .+? on left side of \\?\\?= is not nullable~" # Useful/necessary in the default field resolver, deals with arbitrary user data @@ -45,14 +41,18 @@ includes: services: - - class: GraphQL\Tests\PhpStan\Type\Definition\Type\IsInputTypeStaticMethodTypeSpecifyingExtension + class: GraphQL\Tests\PhpStan\Type\Definition\Type\IsAbstractTypeStaticMethodTypeSpecifyingExtension tags: - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension - - class: GraphQL\Tests\PhpStan\Type\Definition\Type\IsOutputTypeStaticMethodTypeSpecifyingExtension + class: GraphQL\Tests\PhpStan\Type\Definition\Type\IsCompositeTypeStaticMethodTypeSpecifyingExtension tags: - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension - - class: GraphQL\Tests\PhpStan\Type\Definition\Type\IsCompositeTypeStaticMethodTypeSpecifyingExtension + class: GraphQL\Tests\PhpStan\Type\Definition\Type\IsInputTypeStaticMethodTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension + - + class: GraphQL\Tests\PhpStan\Type\Definition\Type\IsOutputTypeStaticMethodTypeSpecifyingExtension tags: - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension diff --git a/src/Deferred.php b/src/Deferred.php index fe406f6df..b4f812879 100644 --- a/src/Deferred.php +++ b/src/Deferred.php @@ -6,10 +6,13 @@ use GraphQL\Executor\Promise\Adapter\SyncPromise; +/** + * @phpstan-import-type Executor from SyncPromise + */ class Deferred extends SyncPromise { /** - * @param callable() : mixed $executor + * @param Executor $executor */ public static function create(callable $executor): self { @@ -17,7 +20,7 @@ public static function create(callable $executor): self } /** - * @param callable() : mixed $executor + * @param Executor $executor */ public function __construct(callable $executor) { diff --git a/src/Error/Error.php b/src/Error/Error.php index ca556875a..61ab12173 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -71,7 +71,7 @@ class Error extends Exception implements JsonSerializable, ClientAware, Provides protected ?array $extensions; /** - * @param iterable|Node|null $nodes + * @param iterable|Node|null $nodes * @param array|null $positions * @param array|null $path * @param array|null $extensions @@ -89,9 +89,9 @@ public function __construct( // Compute list of blame nodes. if ($nodes instanceof Traversable) { - $this->nodes = iterator_to_array($nodes); + $this->nodes = array_filter(iterator_to_array($nodes)); } elseif (is_array($nodes)) { - $this->nodes = $nodes; + $this->nodes = array_filter($nodes); } elseif (null !== $nodes) { $this->nodes = [$nodes]; } else { @@ -160,7 +160,7 @@ public static function createLocatedError($error, $nodes = null, ?array $path = $message = (string) $error; } - $nonEmptyMessage = '' === $message || null === $message + $nonEmptyMessage = '' === $message ? 'An unknown error occurred.' : $message; diff --git a/src/Error/FormattedError.php b/src/Error/FormattedError.php index 8d3e07dae..9b5e48bb2 100644 --- a/src/Error/FormattedError.php +++ b/src/Error/FormattedError.php @@ -6,7 +6,6 @@ use function addcslashes; use function array_filter; -use function array_intersect_key; use function array_map; use function array_merge; use function array_shift; @@ -16,7 +15,6 @@ use function get_class; use function gettype; use GraphQL\Executor\ExecutionResult; -use GraphQL\Language\AST\Node; use GraphQL\Language\Source; use GraphQL\Language\SourceLocation; use GraphQL\Type\Definition\Type; @@ -37,12 +35,7 @@ * It converts PHP exceptions to [spec-compliant errors](https://facebook.github.io/graphql/#sec-Errors) * and provides tools for error debugging. * - * @phpstan-type FormattedErrorArray array{ - * message: string, - * locations?: array, - * path?: array, - * extensions?: array, - * } + * @phpstan-import-type SerializableError from ExecutionResult * @phpstan-import-type ErrorFormatter from ExecutionResult */ class FormattedError @@ -67,20 +60,19 @@ public static function setInternalErrorMessage(string $msg): void public static function printError(Error $error): string { $printedLocations = []; - if (0 !== count($error->nodes ?? [])) { - /** @var Node $node */ - foreach ($error->nodes as $node) { - if (null === $node->loc) { - continue; - } - if (null === $node->loc->source) { + $nodes = $error->nodes; + if (isset($nodes) && count($nodes) > 0) { + foreach ($nodes as $node) { + if (! isset($node->loc->source)) { continue; } + $location = $node->loc; + $source = $location->source; $printedLocations[] = self::highlightSourceAtLocation( - $node->loc->source, - $node->loc->source->getLocation($node->loc->start) + $source, + $source->getLocation($location->start) ); } } elseif (null !== $error->getSource() && 0 !== count($error->getLocations())) { @@ -110,7 +102,9 @@ private static function highlightSourceAtLocation(Source $source, SourceLocation $lineNum = (string) $contextLine; $nextLineNum = (string) ($contextLine + 1); $padLen = strlen($nextLineNum); + $lines = preg_split('/\r\n|[\n\r]/', $source->body); + assert(is_array($lines), 'given the regex is valid'); $lines[0] = self::whitespace($source->locationOffset->column - 1) . $lines[0]; @@ -150,7 +144,7 @@ private static function lpad(int $len, string $str): string * * For a list of available debug flags @see \GraphQL\Error\DebugFlag constants. * - * @return FormattedErrorArray + * @return SerializableError * * @api */ @@ -195,10 +189,10 @@ public static function createFromException(Throwable $exception, int $debugFlag /** * Decorates spec-compliant $formattedError with debug entries according to $debug flags. * - * @param int $debugFlag For available flags @see \GraphQL\Error\DebugFlag - * @param FormattedErrorArray $formattedError + * @param SerializableError $formattedError + * @param int $debugFlag For available flags @see \GraphQL\Error\DebugFlag * - * @return FormattedErrorArray + * @return SerializableError */ public static function addDebugEntries(array $formattedError, Throwable $e, int $debugFlag): array { @@ -264,8 +258,8 @@ public static function prepareFormatter(?callable $formatter, int $debug): calla * Returns error trace as serializable array. * * @return array @@ -287,26 +281,34 @@ public static function toSafeTrace(Throwable $error): array array_shift($trace); } - return array_map( - static function (array $err): array { - $safeErr = array_intersect_key($err, ['file' => true, 'line' => true]); + $formatted = []; + foreach ($trace as $err) { + $safeErr = []; + + if (isset($err['file'])) { + $safeErr['file'] = $err['file']; + } + + if (isset($err['line'])) { + $safeErr['line'] = $err['line']; + } - if (isset($err['function'])) { - $func = $err['function']; - $args = array_map([self::class, 'printVar'], $err['args'] ?? []); - $funcStr = $func . '(' . implode(', ', $args) . ')'; + if (isset($err['function'])) { + $func = $err['function']; + $args = array_map([self::class, 'printVar'], $err['args'] ?? []); + $funcStr = $func . '(' . implode(', ', $args) . ')'; - if (isset($err['class'])) { - $safeErr['call'] = $err['class'] . '::' . $funcStr; - } else { - $safeErr['function'] = $funcStr; - } + if (isset($err['class'])) { + $safeErr['call'] = $err['class'] . '::' . $funcStr; + } else { + $safeErr['function'] = $funcStr; } + } + + $formatted[] = $safeErr; + } - return $safeErr; - }, - $trace - ); + return $formatted; } /** diff --git a/src/Error/UserError.php b/src/Error/UserError.php index d9aefbb55..286112531 100644 --- a/src/Error/UserError.php +++ b/src/Error/UserError.php @@ -7,7 +7,7 @@ use RuntimeException; /** - * Error caused by actions of GraphQL clients. Can be safely displayed to a client... + * Caused by GraphQL clients and can safely be displayed. */ class UserError extends RuntimeException implements ClientAware { diff --git a/src/Executor/ExecutionContext.php b/src/Executor/ExecutionContext.php index db45bb7a8..658537739 100644 --- a/src/Executor/ExecutionContext.php +++ b/src/Executor/ExecutionContext.php @@ -36,7 +36,10 @@ class ExecutionContext /** @var array */ public array $variableValues; - /** @var FieldResolver */ + /** + * @var callable + * @phpstan-var FieldResolver + */ public $fieldResolver; /** @var array */ diff --git a/src/Executor/ExecutionResult.php b/src/Executor/ExecutionResult.php index cd918de08..74039907a 100644 --- a/src/Executor/ExecutionResult.php +++ b/src/Executor/ExecutionResult.php @@ -21,7 +21,12 @@ * Could be converted to [spec-compliant](https://facebook.github.io/graphql/#sec-Response-Format) * serializable array using `toArray()`. * - * @phpstan-type SerializableError array + * @phpstan-type SerializableError array{ + * message: string, + * locations?: array, + * path?: array, + * extensions?: array, + * } * @phpstan-type SerializableErrors array * @phpstan-type SerializableResult array{ * data?: array, diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index f1ea8ab96..352b36081 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -19,16 +19,22 @@ * Implements the "Evaluating requests" section of the GraphQL specification. * * @phpstan-type FieldResolver callable(mixed, array, mixed, ResolveInfo): mixed - * @phpstan-type ImplementationFactory callable(PromiseAdapter, Schema, DocumentNode, mixed=, mixed=, ?array=, ?string=, ?callable=): ExecutorImplementation + * @phpstan-type ImplementationFactory callable(PromiseAdapter, Schema, DocumentNode, mixed, mixed, array, ?string, callable): ExecutorImplementation */ class Executor { - /** @var FieldResolver */ + /** + * @var callable + * @phpstan-var FieldResolver + */ private static $defaultFieldResolver = [self::class, 'defaultFieldResolver']; private static ?PromiseAdapter $defaultPromiseAdapter; - /** @var ImplementationFactory */ + /** + * @var callable + * @phpstan-var ImplementationFactory + */ private static $implementationFactory = [ReferenceExecutor::class, 'create']; /** @@ -91,8 +97,6 @@ public static function setImplementationFactory(callable $implementationFactory) * @param array|null $variableValues * @phpstan-param FieldResolver|null $fieldResolver * - * @return ExecutionResult|array - * * @api */ public static function execute( @@ -103,7 +107,7 @@ public static function execute( ?array $variableValues = null, ?string $operationName = null, ?callable $fieldResolver = null - ) { + ): ExecutionResult { $promiseAdapter = new SyncPromiseAdapter(); $result = static::promiseToExecute( @@ -143,7 +147,6 @@ public static function promiseToExecute( ?string $operationName = null, ?callable $fieldResolver = null ): Promise { - /** @var ExecutorImplementation $executor */ $executor = (self::$implementationFactory)( $promiseAdapter, $schema, diff --git a/src/Executor/Promise/Adapter/AmpPromiseAdapter.php b/src/Executor/Promise/Adapter/AmpPromiseAdapter.php index 44f3abd94..6d621e0ec 100644 --- a/src/Executor/Promise/Adapter/AmpPromiseAdapter.php +++ b/src/Executor/Promise/Adapter/AmpPromiseAdapter.php @@ -41,8 +41,9 @@ public function then(Promise $promise, ?callable $onFulfilled = null, ?callable } }; - /** @var AmpPromise $adoptedPromise */ $adoptedPromise = $promise->adoptedPromise; + assert($adoptedPromise instanceof AmpPromise); + $adoptedPromise->onResolve($onResolve); return new Promise($deferred->promise(), $this); @@ -84,7 +85,9 @@ public function all(array $promisesOrValues): Promise $promises = []; foreach ($promisesOrValues as $key => $item) { if ($item instanceof Promise) { - $promises[$key] = $item->adoptedPromise; + $ampPromise = $item->adoptedPromise; + assert($ampPromise instanceof AmpPromise); + $promises[$key] = $ampPromise; } elseif ($item instanceof AmpPromise) { $promises[$key] = $item; } @@ -108,12 +111,12 @@ public function all(array $promisesOrValues): Promise } /** - * @param Deferred $deferred - * @param callable(TArgument): TResult $callback - * @param TArgument $argument - * * @template TArgument * @template TResult + * + * @param Deferred $deferred + * @param callable(TArgument): TResult $callback + * @param TArgument $argument */ private static function resolveWithCallable(Deferred $deferred, callable $callback, $argument): void { @@ -126,6 +129,7 @@ private static function resolveWithCallable(Deferred $deferred, callable $callba } if ($result instanceof Promise) { + /** @var TResult $result */ $result = $result->adoptedPromise; } diff --git a/src/Executor/Promise/Adapter/ReactPromiseAdapter.php b/src/Executor/Promise/Adapter/ReactPromiseAdapter.php index 888d9608b..8bdce962b 100644 --- a/src/Executor/Promise/Adapter/ReactPromiseAdapter.php +++ b/src/Executor/Promise/Adapter/ReactPromiseAdapter.php @@ -27,8 +27,8 @@ public function convertThenable($thenable): Promise public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise { - /** @var ReactPromiseInterface $adoptedPromise */ $adoptedPromise = $promise->adoptedPromise; + assert($adoptedPromise instanceof ReactPromiseInterface); return new Promise($adoptedPromise->then($onFulfilled, $onRejected), $this); } diff --git a/src/Executor/Promise/Adapter/SyncPromise.php b/src/Executor/Promise/Adapter/SyncPromise.php index 5dfd3c131..869171a17 100644 --- a/src/Executor/Promise/Adapter/SyncPromise.php +++ b/src/Executor/Promise/Adapter/SyncPromise.php @@ -22,6 +22,8 @@ * Root SyncPromise without explicit $executor will never resolve (actually throw while trying). * The whole point of Deferred is to ensure it never happens and that any resolver creates * at least one $executor to start the promise chain. + * + * @phpstan-type Executor callable(): mixed */ class SyncPromise { @@ -58,7 +60,7 @@ public static function runQueue(): void } /** - * @param (callable(): mixed)|null $executor + * @param Executor|null $executor */ public function __construct(?callable $executor = null) { @@ -146,7 +148,6 @@ private function enqueueWaitingPromises(): void foreach ($this->waiting as $descriptor) { self::getQueue()->enqueue(function () use ($descriptor): void { - /** @var self $promise */ [$promise, $onFulfilled, $onRejected] = $descriptor; if (self::FULFILLED === $this->state) { @@ -183,7 +184,7 @@ public static function getQueue(): SplQueue } /** - * @param (callable(mixed): mixed)|null $onFulfilled + * @param (callable(mixed): mixed)|null $onFulfilled * @param (callable(Throwable): mixed)|null $onRejected */ public function then(?callable $onFulfilled = null, ?callable $onRejected = null): self @@ -207,7 +208,7 @@ public function then(?callable $onFulfilled = null, ?callable $onRejected = null } /** - * @param callable(Throwable) : mixed $onRejected + * @param callable(Throwable): mixed $onRejected */ public function catch(callable $onRejected): self { diff --git a/src/Executor/Promise/Adapter/SyncPromiseAdapter.php b/src/Executor/Promise/Adapter/SyncPromiseAdapter.php index 41d23ed14..33fd5f37f 100644 --- a/src/Executor/Promise/Adapter/SyncPromiseAdapter.php +++ b/src/Executor/Promise/Adapter/SyncPromiseAdapter.php @@ -38,8 +38,8 @@ public function convertThenable($thenable): Promise public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise { - /** @var SyncPromise $adoptedPromise */ $adoptedPromise = $promise->adoptedPromise; + assert($adoptedPromise instanceof SyncPromise); return new Promise($adoptedPromise->then($onFulfilled, $onRejected), $this); } @@ -120,17 +120,17 @@ public function wait(Promise $promise) $this->beforeWait($promise); $taskQueue = SyncPromise::getQueue(); + $syncPromise = $promise->adoptedPromise; + assert($syncPromise instanceof SyncPromise); + while ( - SyncPromise::PENDING === $promise->adoptedPromise->state + SyncPromise::PENDING === $syncPromise->state && ! $taskQueue->isEmpty() ) { SyncPromise::runQueue(); $this->onWait($promise); } - /** @var SyncPromise $syncPromise */ - $syncPromise = $promise->adoptedPromise; - if (SyncPromise::FULFILLED === $syncPromise->state) { return $syncPromise->result; } diff --git a/src/Executor/Promise/PromiseAdapter.php b/src/Executor/Promise/PromiseAdapter.php index 76cf789dd..dc0c92cbe 100644 --- a/src/Executor/Promise/PromiseAdapter.php +++ b/src/Executor/Promise/PromiseAdapter.php @@ -23,7 +23,7 @@ public function isThenable($value): bool; /** * Converts thenable of the underlying platform into GraphQL\Executor\Promise\Promise instance. * - * @param object $thenable + * @param mixed $thenable * * @api */ diff --git a/src/Executor/ReferenceExecutor.php b/src/Executor/ReferenceExecutor.php index 1376c5840..a2f812124 100644 --- a/src/Executor/ReferenceExecutor.php +++ b/src/Executor/ReferenceExecutor.php @@ -39,7 +39,6 @@ use GraphQL\Type\Definition\OutputType; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; -use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Introspection; use GraphQL\Type\Schema; use GraphQL\Utils\TypeInfo; @@ -56,7 +55,6 @@ /** * @phpstan-import-type FieldResolver from Executor - * @phpstan-import-type AbstractTypeAlias from AbstractType * @phpstan-type Fields ArrayObject> */ class ReferenceExecutor implements ExecutorImplementation @@ -90,8 +88,8 @@ protected function __construct(ExecutionContext $context) } /** - * @param mixed $rootValue - * @param mixed $contextValue + * @param mixed $rootValue + * @param mixed $contextValue * @param array $variableValues * @phpstan-param FieldResolver $fieldResolver */ @@ -249,22 +247,30 @@ public function doExecute(): Promise // Note: we deviate here from the reference implementation a bit by always returning promise // But for the "sync" case it is always fulfilled - return $this->isPromise($result) - ? $result - : $this->exeContext->promiseAdapter->createFulfilled($result); + + $promise = $this->getPromise($result); + if (null !== $promise) { + return $promise; + } + + return $this->exeContext->promiseAdapter->createFulfilled($result); } /** - * @param mixed|Promise|null $data + * @param mixed $data * * @return ExecutionResult|Promise */ protected function buildResponse($data) { - if ($this->isPromise($data)) { - return $data->then(function ($resolved) { - return $this->buildResponse($resolved); - }); + if ($data instanceof Promise) { + return $data->then(fn ($resolved) => $this->buildResponse($resolved)); + } + + $promiseAdapter = $this->exeContext->promiseAdapter; + if ($promiseAdapter->isThenable($data)) { + return $promiseAdapter->convertThenable($data) + ->then(fn ($resolved) => $this->buildResponse($resolved)); } if (null !== $data) { @@ -295,19 +301,10 @@ protected function executeOperation(OperationDefinitionNode $operation, $rootVal $result = 'mutation' === $operation->operation ? $this->executeFieldsSerially($type, $rootValue, $path, $fields) : $this->executeFields($type, $rootValue, $path, $fields); - if ($this->isPromise($result)) { - return $result->then( - null, - function ($error): ?Promise { - if ($error instanceof Error) { - $this->exeContext->addError($error); - - return $this->exeContext->promiseAdapter->createFulfilled(null); - } - return null; - } - ); + $promise = $this->getPromise($result); + if (null !== $promise) { + return $promise->then(null, [$this, 'onError']); } return $result; @@ -318,6 +315,20 @@ function ($error): ?Promise { } } + /** + * @param mixed $error + */ + public function onError($error): ?Promise + { + if ($error instanceof Error) { + $this->exeContext->addError($error); + + return $this->exeContext->promiseAdapter->createFulfilled(null); + } + + return null; + } + /** * Extracts the root type of the operation from the schema. * @@ -543,10 +554,11 @@ function ($results, $responseName) use ($path, $parentType, $rootValue, $fields) [] ); - if ($this->isPromise($result)) { - return $result->then(static function ($resolvedResults) { - return static::fixResultsIfEmptyArray($resolvedResults); - }); + $promise = $this->getPromise($result); + if (null !== $promise) { + return $result->then( + static fn ($resolvedResults) => static::fixResultsIfEmptyArray($resolvedResults) + ); } return static::fixResultsIfEmptyArray($result); @@ -771,7 +783,7 @@ protected function handleFieldError($rawError, ArrayObject $fieldNodes, array $p * definition. * * If the field is an abstract type, determine the runtime type of the value - * and then complete based on that type + * and then complete based on that type. * * Otherwise, the field type expects a sub-selection set, and will complete the * value by evaluating all sub-selections. @@ -831,7 +843,7 @@ protected function completeValue( return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result); } - /** @var Type&NamedType $returnType wrapping types returned early */ + assert($returnType instanceof NamedType, 'Wrapping types should return early'); // Account for invalid schema definition when typeLoader returns different // instance than `resolveType` or $field->getType() or $arg->getType() @@ -885,8 +897,9 @@ protected function getPromise($value): ?Promise return $value; } - if ($this->exeContext->promiseAdapter->isThenable($value)) { - return $this->exeContext->promiseAdapter->convertThenable($value); + $promiseAdapter = $this->exeContext->promiseAdapter; + if ($promiseAdapter->isThenable($value)) { + return $promiseAdapter->convertThenable($value); } return null; @@ -925,10 +938,10 @@ function ($previous, $value) use ($callback) { /** * Complete a list value by completing each item in the list with the inner type. * - * @param ListOfType $returnType + * @param ListOfType $returnType * @param ArrayObject $fieldNodes - * @param list $path - * @param iterable $results + * @param list $path + * @param iterable $results * * @throws Exception * @@ -990,10 +1003,10 @@ protected function completeLeafValue(LeafType $returnType, &$result) * Complete a value of an abstract type by determining the runtime object type * of that value, then complete the value for that type. * + * @param AbstractType&Type $returnType * @param ArrayObject $fieldNodes - * @param array $path - * @param array $result - * @phpstan-param AbstractTypeAlias $returnType + * @param array $path + * @param array $result * * @throws Error * @@ -1012,7 +1025,7 @@ protected function completeAbstractValue( if (null === $typeCandidate) { $runtimeType = static::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType); } elseif (is_callable($typeCandidate)) { - $runtimeType = Schema::resolveType($typeCandidate); + $runtimeType = $typeCandidate(); } else { $runtimeType = $typeCandidate; } @@ -1065,9 +1078,9 @@ protected function completeAbstractValue( * Otherwise, test each possible type for the abstract type by calling * isTypeOf for the object being coerced, returning the first type that matches. * - * @param mixed|null $value - * @param mixed|null $contextValue - * @param AbstractTypeAlias $abstractType + * @param mixed|null $value + * @param mixed|null $contextValue + * @param AbstractType&Type $abstractType * * @return Promise|Type|string|null */ @@ -1109,13 +1122,14 @@ protected function defaultTypeResolver($value, $contextValue, ResolveInfo $info, $promise = $this->getPromise($isTypeOfResult); if (null !== $promise) { $promisedIsTypeOfResults[$index] = $promise; - } elseif ($isTypeOfResult) { + } elseif (true === $isTypeOfResult) { return $type; } } if (count($promisedIsTypeOfResults) > 0) { - return $this->exeContext->promiseAdapter->all($promisedIsTypeOfResults) + return $this->exeContext->promiseAdapter + ->all($promisedIsTypeOfResults) ->then(static function ($isTypeOfResults) use ($possibleTypes): ?ObjectType { foreach ($isTypeOfResults as $index => $result) { if ($result) { @@ -1174,6 +1188,7 @@ protected function completeObjectValue( }); } + assert(is_bool($isTypeOf), 'Promise would return early'); if (! $isTypeOf) { throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes); } @@ -1338,9 +1353,9 @@ protected function promiseForAssocArray(array $assoc): Promise } /** - * @param string|ObjectType|null $runtimeTypeOrName - * @param InterfaceType|UnionType $returnType - * @param mixed $result + * @param mixed $runtimeTypeOrName + * @param AbstractType&Type $returnType + * @param mixed $result */ protected function ensureValidRuntimeType( $runtimeTypeOrName, @@ -1352,48 +1367,29 @@ protected function ensureValidRuntimeType( ? $this->exeContext->schema->getType($runtimeTypeOrName) : $runtimeTypeOrName; if (! $runtimeType instanceof ObjectType) { + $safeResult = Utils::printSafe($result); + $notObjectType = Utils::printSafe($runtimeType); + throw new InvariantViolation( - sprintf( - 'Abstract type %s must resolve to an Object type at ' - . 'runtime for field %s.%s with value "%s", received "%s". ' - . 'Either the %s type should provide a "resolveType" ' - . 'function or each possible type should provide an "isTypeOf" function.', - $returnType, - $info->parentType, - $info->fieldName, - Utils::printSafe($result), - Utils::printSafe($runtimeType), - $returnType - ) + "Abstract type {$returnType} must resolve to an Object type at runtime for field {$info->parentType}.{$info->fieldName} with value \"{$safeResult}\", received \"{$notObjectType}\". Either the {$returnType} type should provide a \"resolveType\" function or each possible type should provide an \"isTypeOf\" function." ); } if (! $this->exeContext->schema->isSubType($returnType, $runtimeType)) { throw new InvariantViolation( - sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeType, $returnType) + "Runtime Object type \"{$runtimeType}\" is not a possible type for \"{$returnType}\"." ); } if (null === $this->exeContext->schema->getType($runtimeType->name)) { throw new InvariantViolation( - 'Schema does not contain type "' . $runtimeType->name . '". ' - . 'This can happen when an object type is only referenced indirectly through ' - . 'abstract types and never directly through fields.' - . 'List the type in the option "types" during schema construction, ' - . 'see https://webonyx.github.io/graphql-php/type-system/schema/#configuration-options.' + "Schema does not contain type \"{$runtimeType}\". This can happen when an object type is only referenced indirectly through abstract types and never directly through fields.List the type in the option \"types\" during schema construction, see https://webonyx.github.io/graphql-php/type-system/schema/#configuration-options." ); } if ($runtimeType !== $this->exeContext->schema->getType($runtimeType->name)) { throw new InvariantViolation( - sprintf( - 'Schema must contain unique named types but contains multiple types named "%s". ' - . 'Make sure that `resolveType` function of abstract type "%s" returns the same ' - . 'type instance as referenced anywhere else within the schema ' - . '(see https://webonyx.github.io/graphql-php/type-definitions/#type-registry).', - $runtimeType, - $returnType - ) + "Schema must contain unique named types but contains multiple types named \"{$runtimeType}\". Make sure that `resolveType` function of abstract type \"{$returnType}\" returns the same type instance as referenced anywhere else within the schema (see https://webonyx.github.io/graphql-php/type-definitions/#type-registry)." ); } diff --git a/src/Executor/Values.php b/src/Executor/Values.php index 1ce1a0a63..c3d25c1c5 100644 --- a/src/Executor/Values.php +++ b/src/Executor/Values.php @@ -22,7 +22,6 @@ use GraphQL\Language\Printer; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\FieldDefinition; -use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; @@ -45,7 +44,7 @@ class Values * to match the variable definitions, an Error will be thrown. * * @param NodeList $varDefNodes - * @param array $rawVariableValues + * @param array $rawVariableValues * * @return array{array, null}|array{null, array} */ @@ -60,16 +59,12 @@ public static function getVariableValues(Schema $schema, NodeList $varDefNodes, if (! Type::isInputType($varType)) { // Must use input types for variables. This should be caught during // validation, however is checked again here for safety. + $typeStr = Printer::doPrint($varDefNode->type); $errors[] = new Error( - sprintf( - 'Variable "$%s" expected value of type "%s" which cannot be used as an input type.', - $varName, - Printer::doPrint($varDefNode->type) - ), + "Variable \"\${$varName}\" expected value of type \"{$typeStr}\" which cannot be used as an input type.", [$varDefNode->type] ); } else { - /** @var InputType&Type $varType */ $hasValue = array_key_exists($varName, $rawVariableValues); $value = $hasValue ? $rawVariableValues[$varName] @@ -140,11 +135,11 @@ public static function getVariableValues(Schema $schema, NodeList $varDefNodes, * If the directive does not exist on the node, returns undefined. * * @param FragmentSpreadNode|FieldNode|InlineFragmentNode|EnumValueDefinitionNode|FieldDefinitionNode $node - * @param mixed[]|null $variableValues + * @param array|null $variableValues * * @return array|null */ - public static function getDirectiveValues(Directive $directiveDef, $node, $variableValues = null): ?array + public static function getDirectiveValues(Directive $directiveDef, Node $node, ?array $variableValues = null): ?array { $directiveNode = Utils::find( $node->directives, @@ -165,23 +160,23 @@ static function (DirectiveNode $directive) use ($directiveDef): bool { * definitions and list of argument AST nodes. * * @param FieldDefinition|Directive $def - * @param FieldNode|DirectiveNode $node - * @param mixed[] $variableValues + * @param FieldNode|DirectiveNode $node + * @param array|null $variableValues * * @throws Error * * @return array */ - public static function getArgumentValues($def, $node, $variableValues = null): array + public static function getArgumentValues($def, Node $node, ?array $variableValues = null): array { if (0 === count($def->args)) { return []; } - $argumentNodes = $node->arguments; /** @var array $argumentValueMap */ $argumentValueMap = []; - foreach ($argumentNodes as $argumentNode) { + + foreach ($node->arguments as $argumentNode) { $argumentValueMap[$argumentNode->name->value] = $argumentNode->value; } @@ -189,28 +184,27 @@ public static function getArgumentValues($def, $node, $variableValues = null): a } /** - * @param FieldDefinition|Directive $fieldDefinition + * @param FieldDefinition|Directive $def * @param array $argumentValueMap - * @param mixed[] $variableValues - * @param Node|null $referenceNode + * @param array|null $variableValues * * @throws Error * - * @return mixed[] + * @return array */ - public static function getArgumentValuesForMap($fieldDefinition, array $argumentValueMap, $variableValues = null, $referenceNode = null): array + public static function getArgumentValuesForMap($def, array $argumentValueMap, ?array $variableValues = null, ?Node $referenceNode = null): array { - $argumentDefinitions = $fieldDefinition->args; + /** @var array $coercedValues */ $coercedValues = []; - foreach ($argumentDefinitions as $argumentDefinition) { + foreach ($def->args as $argumentDefinition) { $name = $argumentDefinition->name; $argType = $argumentDefinition->getType(); $argumentValueNode = $argumentValueMap[$name] ?? null; if ($argumentValueNode instanceof VariableNode) { $variableName = $argumentValueNode->name->value; - $hasValue = array_key_exists($variableName, $variableValues ?? []); + $hasValue = null !== $variableValues && array_key_exists($variableName, $variableValues); $isNull = $hasValue ? null === $variableValues[$variableName] : false; @@ -263,14 +257,14 @@ public static function getArgumentValuesForMap($fieldDefinition, array $argument // usage here is of the correct type. $coercedValues[$name] = $variableValues[$variableName] ?? null; } else { - $valueNode = $argumentValueNode; - $coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues); + $coercedValue = AST::valueFromAST($argumentValueNode, $argType, $variableValues); if (Utils::isInvalid($coercedValue)) { // Note: ValuesOfCorrectType validation should catch this before // execution. This is a runtime check to ensure execution does not // continue with an invalid argument value. + $invalidValue = Printer::doPrint($argumentValueNode); throw new Error( - 'Argument "' . $name . '" has invalid value ' . Printer::doPrint($valueNode) . '.', + "Argument \"{$name}\" has invalid value {$invalidValue}.", [$argumentValueNode] ); } diff --git a/src/GraphQL.php b/src/GraphQL.php index f07aee126..9a93f258c 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -130,10 +130,9 @@ public static function promiseToExecute( ? $source : Parser::parse(new Source($source, 'GraphQL')); - // TODO this could be more elegant - if (0 === count($validationRules ?? [])) { - /** @var QueryComplexity $queryComplexity */ + if (null === $validationRules || 0 === count($validationRules)) { $queryComplexity = DocumentValidator::getRule(QueryComplexity::class); + assert($queryComplexity instanceof QueryComplexity, 'should not register a different rule for QueryComplexity'); $queryComplexity->setRawVariableValues($variableValues); } else { foreach ($validationRules as $rule) { diff --git a/src/Language/AST/ArgumentNode.php b/src/Language/AST/ArgumentNode.php index d83870724..7aa245c99 100644 --- a/src/Language/AST/ArgumentNode.php +++ b/src/Language/AST/ArgumentNode.php @@ -11,9 +11,8 @@ class ArgumentNode extends Node { public string $kind = NodeKind::ARGUMENT; - /** @var ArgumentNodeValue */ - public $value; + /** @phpstan-var ArgumentNodeValue */ + public ValueNode $value; - /** @var NameNode */ - public $name; + public NameNode $name; } diff --git a/src/Language/AST/BooleanValueNode.php b/src/Language/AST/BooleanValueNode.php index 47d7c4475..86c24f261 100644 --- a/src/Language/AST/BooleanValueNode.php +++ b/src/Language/AST/BooleanValueNode.php @@ -8,6 +8,5 @@ class BooleanValueNode extends Node implements ValueNode { public string $kind = NodeKind::BOOLEAN; - /** @var bool */ - public $value; + public bool $value; } diff --git a/src/Language/AST/DirectiveDefinitionNode.php b/src/Language/AST/DirectiveDefinitionNode.php index d0cc4f6da..d9e20a7ae 100644 --- a/src/Language/AST/DirectiveDefinitionNode.php +++ b/src/Language/AST/DirectiveDefinitionNode.php @@ -8,18 +8,15 @@ class DirectiveDefinitionNode extends Node implements TypeSystemDefinitionNode { public string $kind = NodeKind::DIRECTIVE_DEFINITION; - /** @var NameNode */ - public $name; + public NameNode $name; - /** @var StringValueNode|null */ - public $description; + public ?StringValueNode $description = null; /** @var NodeList */ - public $arguments; + public NodeList $arguments; - /** @var bool */ - public $repeatable; + public bool $repeatable; /** @var NodeList */ - public $locations; + public NodeList $locations; } diff --git a/src/Language/AST/DirectiveNode.php b/src/Language/AST/DirectiveNode.php index 19d5235f0..431d463af 100644 --- a/src/Language/AST/DirectiveNode.php +++ b/src/Language/AST/DirectiveNode.php @@ -8,9 +8,8 @@ class DirectiveNode extends Node { public string $kind = NodeKind::DIRECTIVE; - /** @var NameNode */ - public $name; + public NameNode $name; /** @var NodeList */ - public $arguments; + public NodeList $arguments; } diff --git a/src/Language/AST/DocumentNode.php b/src/Language/AST/DocumentNode.php index acecbc453..f974463bf 100644 --- a/src/Language/AST/DocumentNode.php +++ b/src/Language/AST/DocumentNode.php @@ -9,5 +9,5 @@ class DocumentNode extends Node public string $kind = NodeKind::DOCUMENT; /** @var NodeList */ - public $definitions; + public NodeList $definitions; } diff --git a/src/Language/AST/EnumTypeDefinitionNode.php b/src/Language/AST/EnumTypeDefinitionNode.php index ec19fe13b..9c89965c2 100644 --- a/src/Language/AST/EnumTypeDefinitionNode.php +++ b/src/Language/AST/EnumTypeDefinitionNode.php @@ -8,15 +8,13 @@ class EnumTypeDefinitionNode extends Node implements TypeDefinitionNode { public string $kind = NodeKind::ENUM_TYPE_DEFINITION; - /** @var NameNode */ - public $name; + public NameNode $name; /** @var NodeList */ - public $directives; + public NodeList $directives; /** @var NodeList */ - public $values; + public NodeList $values; - /** @var StringValueNode|null */ - public $description; + public ?StringValueNode $description = null; } diff --git a/src/Language/AST/EnumTypeExtensionNode.php b/src/Language/AST/EnumTypeExtensionNode.php index 55e21a6ed..6f545ba45 100644 --- a/src/Language/AST/EnumTypeExtensionNode.php +++ b/src/Language/AST/EnumTypeExtensionNode.php @@ -8,12 +8,11 @@ class EnumTypeExtensionNode extends Node implements TypeExtensionNode { public string $kind = NodeKind::ENUM_TYPE_EXTENSION; - /** @var NameNode */ - public $name; + public NameNode $name; /** @var NodeList */ - public $directives; + public NodeList $directives; /** @var NodeList */ - public $values; + public NodeList $values; } diff --git a/src/Language/AST/EnumValueDefinitionNode.php b/src/Language/AST/EnumValueDefinitionNode.php index c45184d3c..11c05dada 100644 --- a/src/Language/AST/EnumValueDefinitionNode.php +++ b/src/Language/AST/EnumValueDefinitionNode.php @@ -8,12 +8,10 @@ class EnumValueDefinitionNode extends Node { public string $kind = NodeKind::ENUM_VALUE_DEFINITION; - /** @var NameNode */ - public $name; + public NameNode $name; /** @var NodeList */ - public $directives; + public NodeList $directives; - /** @var StringValueNode|null */ - public $description; + public ?StringValueNode $description = null; } diff --git a/src/Language/AST/EnumValueNode.php b/src/Language/AST/EnumValueNode.php index db3227145..bac94b182 100644 --- a/src/Language/AST/EnumValueNode.php +++ b/src/Language/AST/EnumValueNode.php @@ -8,6 +8,5 @@ class EnumValueNode extends Node implements ValueNode { public string $kind = NodeKind::ENUM; - /** @var string */ - public $value; + public string $value; } diff --git a/src/Language/AST/FieldDefinitionNode.php b/src/Language/AST/FieldDefinitionNode.php index e828bac20..7e082211f 100644 --- a/src/Language/AST/FieldDefinitionNode.php +++ b/src/Language/AST/FieldDefinitionNode.php @@ -8,18 +8,16 @@ class FieldDefinitionNode extends Node { public string $kind = NodeKind::FIELD_DEFINITION; - /** @var NameNode */ - public $name; + public NameNode $name; /** @var NodeList */ - public $arguments; + public NodeList $arguments; /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */ - public $type; + public TypeNode $type; /** @var NodeList */ - public $directives; + public NodeList $directives; - /** @var StringValueNode|null */ - public $description; + public ?StringValueNode $description = null; } diff --git a/src/Language/AST/FloatValueNode.php b/src/Language/AST/FloatValueNode.php index 0bacde57b..f65063767 100644 --- a/src/Language/AST/FloatValueNode.php +++ b/src/Language/AST/FloatValueNode.php @@ -8,6 +8,5 @@ class FloatValueNode extends Node implements ValueNode { public string $kind = NodeKind::FLOAT; - /** @var string */ - public $value; + public string $value; } diff --git a/src/Language/AST/FragmentDefinitionNode.php b/src/Language/AST/FragmentDefinitionNode.php index 89144946b..41796ce82 100644 --- a/src/Language/AST/FragmentDefinitionNode.php +++ b/src/Language/AST/FragmentDefinitionNode.php @@ -8,8 +8,7 @@ class FragmentDefinitionNode extends Node implements ExecutableDefinitionNode, H { public string $kind = NodeKind::FRAGMENT_DEFINITION; - /** @var NameNode */ - public $name; + public NameNode $name; /** * Note: fragment variable definitions are experimental and may be changed @@ -19,14 +18,12 @@ class FragmentDefinitionNode extends Node implements ExecutableDefinitionNode, H * * @var NodeList|null */ - public $variableDefinitions; + public ?NodeList $variableDefinitions = null; - /** @var NamedTypeNode */ - public $typeCondition; + public NamedTypeNode $typeCondition; /** @var NodeList */ - public $directives; + public NodeList $directives; - /** @var SelectionSetNode */ - public $selectionSet; + public SelectionSetNode $selectionSet; } diff --git a/src/Language/AST/FragmentSpreadNode.php b/src/Language/AST/FragmentSpreadNode.php index 5151d766d..840f5f4d8 100644 --- a/src/Language/AST/FragmentSpreadNode.php +++ b/src/Language/AST/FragmentSpreadNode.php @@ -8,9 +8,8 @@ class FragmentSpreadNode extends Node implements SelectionNode { public string $kind = NodeKind::FRAGMENT_SPREAD; - /** @var NameNode */ - public $name; + public NameNode $name; /** @var NodeList */ - public $directives; + public NodeList $directives; } diff --git a/src/Language/AST/InputObjectTypeDefinitionNode.php b/src/Language/AST/InputObjectTypeDefinitionNode.php index 2592c3a56..26098bdc7 100644 --- a/src/Language/AST/InputObjectTypeDefinitionNode.php +++ b/src/Language/AST/InputObjectTypeDefinitionNode.php @@ -8,15 +8,13 @@ class InputObjectTypeDefinitionNode extends Node implements TypeDefinitionNode { public string $kind = NodeKind::INPUT_OBJECT_TYPE_DEFINITION; - /** @var NameNode */ - public $name; + public NameNode $name; /** @var NodeList */ - public $directives; + public NodeList $directives; /** @var NodeList */ - public $fields; + public NodeList $fields; - /** @var StringValueNode|null */ - public $description; + public ?StringValueNode $description = null; } diff --git a/src/Language/AST/InputObjectTypeExtensionNode.php b/src/Language/AST/InputObjectTypeExtensionNode.php index 406577ae7..15da4d92a 100644 --- a/src/Language/AST/InputObjectTypeExtensionNode.php +++ b/src/Language/AST/InputObjectTypeExtensionNode.php @@ -8,12 +8,11 @@ class InputObjectTypeExtensionNode extends Node implements TypeExtensionNode { public string $kind = NodeKind::INPUT_OBJECT_TYPE_EXTENSION; - /** @var NameNode */ - public $name; + public NameNode $name; /** @var NodeList */ - public $directives; + public NodeList $directives; /** @var NodeList */ - public $fields; + public NodeList $fields; } diff --git a/src/Language/AST/InputValueDefinitionNode.php b/src/Language/AST/InputValueDefinitionNode.php index 969b149e4..1b2c68f3f 100644 --- a/src/Language/AST/InputValueDefinitionNode.php +++ b/src/Language/AST/InputValueDefinitionNode.php @@ -8,18 +8,16 @@ class InputValueDefinitionNode extends Node { public string $kind = NodeKind::INPUT_VALUE_DEFINITION; - /** @var NameNode */ - public $name; + public NameNode $name; /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */ - public $type; + public TypeNode $type; /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode|null */ - public $defaultValue; + public ?ValueNode $defaultValue = null; /** @var NodeList */ - public $directives; + public NodeList $directives; - /** @var StringValueNode|null */ - public $description; + public ?StringValueNode $description = null; } diff --git a/src/Language/AST/IntValueNode.php b/src/Language/AST/IntValueNode.php index 9d2003b07..b5d00ec6e 100644 --- a/src/Language/AST/IntValueNode.php +++ b/src/Language/AST/IntValueNode.php @@ -8,6 +8,5 @@ class IntValueNode extends Node implements ValueNode { public string $kind = NodeKind::INT; - /** @var string */ - public $value; + public string $value; } diff --git a/src/Language/AST/InterfaceTypeDefinitionNode.php b/src/Language/AST/InterfaceTypeDefinitionNode.php index 8ccb5387d..16d14ff37 100644 --- a/src/Language/AST/InterfaceTypeDefinitionNode.php +++ b/src/Language/AST/InterfaceTypeDefinitionNode.php @@ -8,18 +8,16 @@ class InterfaceTypeDefinitionNode extends Node implements TypeDefinitionNode { public string $kind = NodeKind::INTERFACE_TYPE_DEFINITION; - /** @var NameNode */ - public $name; + public NameNode $name; /** @var NodeList */ - public $directives; + public NodeList $directives; /** @var NodeList */ - public $interfaces; + public NodeList $interfaces; /** @var NodeList */ - public $fields; + public NodeList $fields; - /** @var StringValueNode|null */ - public $description; + public ?StringValueNode $description = null; } diff --git a/src/Language/AST/InterfaceTypeExtensionNode.php b/src/Language/AST/InterfaceTypeExtensionNode.php index 4b45f68b1..33eb61ce6 100644 --- a/src/Language/AST/InterfaceTypeExtensionNode.php +++ b/src/Language/AST/InterfaceTypeExtensionNode.php @@ -8,15 +8,14 @@ class InterfaceTypeExtensionNode extends Node implements TypeExtensionNode { public string $kind = NodeKind::INTERFACE_TYPE_EXTENSION; - /** @var NameNode */ - public $name; + public NameNode $name; /** @var NodeList */ - public $directives; + public NodeList $directives; /** @var NodeList */ - public $interfaces; + public NodeList $interfaces; /** @var NodeList */ - public $fields; + public NodeList $fields; } diff --git a/src/Language/AST/ListTypeNode.php b/src/Language/AST/ListTypeNode.php index a48e898e8..d619531ee 100644 --- a/src/Language/AST/ListTypeNode.php +++ b/src/Language/AST/ListTypeNode.php @@ -9,5 +9,5 @@ class ListTypeNode extends Node implements TypeNode public string $kind = NodeKind::LIST_TYPE; /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */ - public $type; + public TypeNode $type; } diff --git a/src/Language/AST/ListValueNode.php b/src/Language/AST/ListValueNode.php index df05df08d..11381166d 100644 --- a/src/Language/AST/ListValueNode.php +++ b/src/Language/AST/ListValueNode.php @@ -9,5 +9,5 @@ class ListValueNode extends Node implements ValueNode public string $kind = NodeKind::LST; /** @var NodeList */ - public $values; + public NodeList $values; } diff --git a/src/Language/AST/Location.php b/src/Language/AST/Location.php index 2c62d9fc7..f4e3bfd4d 100644 --- a/src/Language/AST/Location.php +++ b/src/Language/AST/Location.php @@ -28,17 +28,17 @@ class Location /** * The Token at which this Node begins. */ - public ?Token $startToken; + public ?Token $startToken = null; /** * The Token at which this Node ends. */ - public ?Token $endToken; + public ?Token $endToken = null; /** * The Source document the AST represents. */ - public ?Source $source; + public ?Source $source = null; public static function create(int $start, int $end): self { diff --git a/src/Language/AST/NameNode.php b/src/Language/AST/NameNode.php index 95b2ade78..bc019dc78 100644 --- a/src/Language/AST/NameNode.php +++ b/src/Language/AST/NameNode.php @@ -8,6 +8,5 @@ class NameNode extends Node implements TypeNode { public string $kind = NodeKind::NAME; - /** @var string */ - public $value; + public string $value; } diff --git a/src/Language/AST/NamedTypeNode.php b/src/Language/AST/NamedTypeNode.php index 1e77eb8f4..9e5f096d0 100644 --- a/src/Language/AST/NamedTypeNode.php +++ b/src/Language/AST/NamedTypeNode.php @@ -8,6 +8,5 @@ class NamedTypeNode extends Node implements TypeNode { public string $kind = NodeKind::NAMED_TYPE; - /** @var NameNode */ - public $name; + public NameNode $name; } diff --git a/src/Language/AST/Node.php b/src/Language/AST/Node.php index 1ed316eed..ea5580c56 100644 --- a/src/Language/AST/Node.php +++ b/src/Language/AST/Node.php @@ -63,12 +63,12 @@ public function cloneDeep(): self } /** + * @template TNode of Node + * @template TCloneable of TNode|NodeList|Location|string + * * @phpstan-param TCloneable $value * * @phpstan-return TCloneable - * - * @template TNode of Node - * @template TCloneable of TNode|NodeList|Location|string */ protected static function cloneValue($value) { @@ -90,7 +90,7 @@ protected static function cloneValue($value) public function __toString(): string { - return json_encode($this); + return json_encode($this, JSON_THROW_ON_ERROR); } /** diff --git a/src/Language/AST/NodeList.php b/src/Language/AST/NodeList.php index 5f6f0d00d..f0a1f3fc3 100644 --- a/src/Language/AST/NodeList.php +++ b/src/Language/AST/NodeList.php @@ -137,7 +137,7 @@ public function merge(iterable $list): NodeList public function getIterator(): Traversable { foreach ($this->nodes as $key => $_) { - yield $this->offsetGet($key); + yield $key => $this->offsetGet($key); } } @@ -153,8 +153,9 @@ public function count(): int */ public function cloneDeep(): self { - $cloned = clone $this; - foreach ($this->nodes as $key => $node) { + /** @var static $cloned */ + $cloned = new static([]); + foreach ($this->getIterator() as $key => $node) { $cloned[$key] = $node->cloneDeep(); } diff --git a/src/Language/AST/NonNullTypeNode.php b/src/Language/AST/NonNullTypeNode.php index 8f8de7b0c..e8a01e17b 100644 --- a/src/Language/AST/NonNullTypeNode.php +++ b/src/Language/AST/NonNullTypeNode.php @@ -9,5 +9,5 @@ class NonNullTypeNode extends Node implements TypeNode public string $kind = NodeKind::NON_NULL_TYPE; /** @var NamedTypeNode|ListTypeNode */ - public $type; + public TypeNode $type; } diff --git a/src/Language/AST/ObjectFieldNode.php b/src/Language/AST/ObjectFieldNode.php index 7947224bf..abd2592b9 100644 --- a/src/Language/AST/ObjectFieldNode.php +++ b/src/Language/AST/ObjectFieldNode.php @@ -8,9 +8,8 @@ class ObjectFieldNode extends Node { public string $kind = NodeKind::OBJECT_FIELD; - /** @var NameNode */ - public $name; + public NameNode $name; /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode */ - public $value; + public ValueNode $value; } diff --git a/src/Language/AST/ObjectTypeDefinitionNode.php b/src/Language/AST/ObjectTypeDefinitionNode.php index 0c0f73752..2cf71a453 100644 --- a/src/Language/AST/ObjectTypeDefinitionNode.php +++ b/src/Language/AST/ObjectTypeDefinitionNode.php @@ -8,18 +8,16 @@ class ObjectTypeDefinitionNode extends Node implements TypeDefinitionNode { public string $kind = NodeKind::OBJECT_TYPE_DEFINITION; - /** @var NameNode */ - public $name; + public NameNode $name; /** @var NodeList */ - public $interfaces; + public NodeList $interfaces; /** @var NodeList */ - public $directives; + public NodeList $directives; /** @var NodeList */ - public $fields; + public NodeList $fields; - /** @var StringValueNode|null */ - public $description; + public ?StringValueNode $description = null; } diff --git a/src/Language/AST/ObjectTypeExtensionNode.php b/src/Language/AST/ObjectTypeExtensionNode.php index d89092672..13b474307 100644 --- a/src/Language/AST/ObjectTypeExtensionNode.php +++ b/src/Language/AST/ObjectTypeExtensionNode.php @@ -8,15 +8,14 @@ class ObjectTypeExtensionNode extends Node implements TypeExtensionNode { public string $kind = NodeKind::OBJECT_TYPE_EXTENSION; - /** @var NameNode */ - public $name; + public NameNode $name; /** @var NodeList */ - public $interfaces; + public NodeList $interfaces; /** @var NodeList */ - public $directives; + public NodeList $directives; /** @var NodeList */ - public $fields; + public NodeList $fields; } diff --git a/src/Language/AST/ObjectValueNode.php b/src/Language/AST/ObjectValueNode.php index 888557368..f3320fa4e 100644 --- a/src/Language/AST/ObjectValueNode.php +++ b/src/Language/AST/ObjectValueNode.php @@ -9,5 +9,5 @@ class ObjectValueNode extends Node implements ValueNode public string $kind = NodeKind::OBJECT; /** @var NodeList */ - public $fields; + public NodeList $fields; } diff --git a/src/Language/AST/OperationDefinitionNode.php b/src/Language/AST/OperationDefinitionNode.php index b95e78cec..1bcc6bb90 100644 --- a/src/Language/AST/OperationDefinitionNode.php +++ b/src/Language/AST/OperationDefinitionNode.php @@ -10,7 +10,9 @@ class OperationDefinitionNode extends Node implements ExecutableDefinitionNode, public ?NameNode $name = null; - /** @var string (oneOf 'query', 'mutation', 'subscription')) */ + /** + * @var 'query'|'mutation'|'subscription' + */ public string $operation; /** @var NodeList */ diff --git a/src/Language/AST/OperationTypeDefinitionNode.php b/src/Language/AST/OperationTypeDefinitionNode.php index 3afd4edaf..8f36b34f2 100644 --- a/src/Language/AST/OperationTypeDefinitionNode.php +++ b/src/Language/AST/OperationTypeDefinitionNode.php @@ -9,12 +9,9 @@ class OperationTypeDefinitionNode extends Node public string $kind = NodeKind::OPERATION_TYPE_DEFINITION; /** - * One of 'query' | 'mutation' | 'subscription'. - * - * @var string + * @var 'query'|'mutation'|'subscription' */ - public $operation; + public string $operation; - /** @var NamedTypeNode */ - public $type; + public NamedTypeNode $type; } diff --git a/src/Language/AST/ScalarTypeDefinitionNode.php b/src/Language/AST/ScalarTypeDefinitionNode.php index 20b69a0c9..1dd2c1d9a 100644 --- a/src/Language/AST/ScalarTypeDefinitionNode.php +++ b/src/Language/AST/ScalarTypeDefinitionNode.php @@ -8,12 +8,10 @@ class ScalarTypeDefinitionNode extends Node implements TypeDefinitionNode { public string $kind = NodeKind::SCALAR_TYPE_DEFINITION; - /** @var NameNode */ - public $name; + public NameNode $name; /** @var NodeList */ - public $directives; + public NodeList $directives; - /** @var StringValueNode|null */ - public $description; + public ?StringValueNode $description = null; } diff --git a/src/Language/AST/ScalarTypeExtensionNode.php b/src/Language/AST/ScalarTypeExtensionNode.php index 7473caca1..79a7b6233 100644 --- a/src/Language/AST/ScalarTypeExtensionNode.php +++ b/src/Language/AST/ScalarTypeExtensionNode.php @@ -8,9 +8,8 @@ class ScalarTypeExtensionNode extends Node implements TypeExtensionNode { public string $kind = NodeKind::SCALAR_TYPE_EXTENSION; - /** @var NameNode */ - public $name; + public NameNode $name; /** @var NodeList */ - public $directives; + public NodeList $directives; } diff --git a/src/Language/AST/SchemaDefinitionNode.php b/src/Language/AST/SchemaDefinitionNode.php index 1bffd7b18..65893756e 100644 --- a/src/Language/AST/SchemaDefinitionNode.php +++ b/src/Language/AST/SchemaDefinitionNode.php @@ -9,8 +9,8 @@ class SchemaDefinitionNode extends Node implements TypeSystemDefinitionNode public string $kind = NodeKind::SCHEMA_DEFINITION; /** @var NodeList */ - public $directives; + public NodeList $directives; /** @var NodeList */ - public $operationTypes; + public NodeList $operationTypes; } diff --git a/src/Language/AST/SchemaTypeExtensionNode.php b/src/Language/AST/SchemaTypeExtensionNode.php index a8bc46659..bc1313607 100644 --- a/src/Language/AST/SchemaTypeExtensionNode.php +++ b/src/Language/AST/SchemaTypeExtensionNode.php @@ -9,8 +9,8 @@ class SchemaTypeExtensionNode extends Node implements TypeExtensionNode public string $kind = NodeKind::SCHEMA_EXTENSION; /** @var NodeList */ - public $directives; + public NodeList $directives; /** @var NodeList */ - public $operationTypes; + public NodeList $operationTypes; } diff --git a/src/Language/AST/StringValueNode.php b/src/Language/AST/StringValueNode.php index 060c27724..9ceea0d43 100644 --- a/src/Language/AST/StringValueNode.php +++ b/src/Language/AST/StringValueNode.php @@ -8,9 +8,7 @@ class StringValueNode extends Node implements ValueNode { public string $kind = NodeKind::STRING; - /** @var string */ - public $value; + public string $value; - /** @var bool */ - public $block; + public bool $block = false; } diff --git a/src/Language/AST/UnionTypeDefinitionNode.php b/src/Language/AST/UnionTypeDefinitionNode.php index b9768d8c7..9813a82ce 100644 --- a/src/Language/AST/UnionTypeDefinitionNode.php +++ b/src/Language/AST/UnionTypeDefinitionNode.php @@ -8,15 +8,13 @@ class UnionTypeDefinitionNode extends Node implements TypeDefinitionNode { public string $kind = NodeKind::UNION_TYPE_DEFINITION; - /** @var NameNode */ - public $name; + public NameNode $name; /** @var NodeList */ - public $directives; + public NodeList $directives; /** @var NodeList */ - public $types; + public NodeList $types; - /** @var StringValueNode|null */ - public $description; + public ?StringValueNode $description = null; } diff --git a/src/Language/AST/UnionTypeExtensionNode.php b/src/Language/AST/UnionTypeExtensionNode.php index 1f4981f2e..b0c8705bd 100644 --- a/src/Language/AST/UnionTypeExtensionNode.php +++ b/src/Language/AST/UnionTypeExtensionNode.php @@ -8,12 +8,11 @@ class UnionTypeExtensionNode extends Node implements TypeExtensionNode { public string $kind = NodeKind::UNION_TYPE_EXTENSION; - /** @var NameNode */ - public $name; + public NameNode $name; /** @var NodeList */ - public $directives; + public NodeList $directives; /** @var NodeList */ - public $types; + public NodeList $types; } diff --git a/src/Language/AST/VariableDefinitionNode.php b/src/Language/AST/VariableDefinitionNode.php index ad0a06756..aebbc6ce1 100644 --- a/src/Language/AST/VariableDefinitionNode.php +++ b/src/Language/AST/VariableDefinitionNode.php @@ -8,15 +8,14 @@ class VariableDefinitionNode extends Node implements DefinitionNode { public string $kind = NodeKind::VARIABLE_DEFINITION; - /** @var VariableNode */ - public $variable; + public VariableNode $variable; /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */ - public $type; + public TypeNode $type; /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode|null */ - public $defaultValue; + public ?ValueNode $defaultValue = null; /** @var NodeList */ - public $directives; + public NodeList $directives; } diff --git a/src/Language/AST/VariableNode.php b/src/Language/AST/VariableNode.php index 895795dfc..4e958091e 100644 --- a/src/Language/AST/VariableNode.php +++ b/src/Language/AST/VariableNode.php @@ -8,6 +8,5 @@ class VariableNode extends Node implements ValueNode { public string $kind = NodeKind::VARIABLE; - /** @var NameNode */ - public $name; + public NameNode $name; } diff --git a/src/Language/BlockString.php b/src/Language/BlockString.php index 60b6cb606..b0fbcf951 100644 --- a/src/Language/BlockString.php +++ b/src/Language/BlockString.php @@ -26,6 +26,7 @@ public static function dedentValue(string $rawString): string { // Expand a block string's raw value into independent lines. $lines = preg_split('/\\r\\n|[\\n\\r]/', $rawString); + assert(is_array($lines), 'given the regex is valid'); // Remove common indentation from all lines but first. $commonIndent = self::getIndentation($rawString); diff --git a/src/Language/Lexer.php b/src/Language/Lexer.php index b999d072a..c3ab6a2d3 100644 --- a/src/Language/Lexer.php +++ b/src/Language/Lexer.php @@ -504,7 +504,7 @@ private function readString(int $line, int $col, Token $prev): Token case 117: $position = $this->position; [$hex] = $this->readChars(4, true); - if (! preg_match('/[0-9a-fA-F]{4}/', $hex)) { + if (1 !== preg_match('/[0-9a-fA-F]{4}/', $hex)) { throw new SyntaxError( $this->source, $position - 1, @@ -513,12 +513,13 @@ private function readString(int $line, int $col, Token $prev): Token } $code = hexdec($hex); + assert(is_int($code), 'Since only a single char is read'); // UTF-16 surrogate pair detection and handling. $highOrderByte = $code >> 8; if (0xD8 <= $highOrderByte && $highOrderByte <= 0xDF) { [$utf16Continuation] = $this->readChars(6, true); - if (! preg_match('/^\\\u[0-9a-fA-F]{4}$/', $utf16Continuation)) { + if (1 !== preg_match('/^\\\u[0-9a-fA-F]{4}$/', $utf16Continuation)) { throw new SyntaxError( $this->source, $this->position - 5, diff --git a/src/Language/Parser.php b/src/Language/Parser.php index 7c7228e01..9f6fd2ed4 100644 --- a/src/Language/Parser.php +++ b/src/Language/Parser.php @@ -206,9 +206,9 @@ public static function parse($source, array $options = []): DocumentNode * Consider providing the results to the utility function: `GraphQL\Utils\AST::valueFromAST()`. * * @param Source|string $source - * @phpstan-param ParserOptions $options + * @phpstan-param ParserOptions $options * - * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|ObjectValueNode|StringValueNode|VariableNode + * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|NullValueNode|ObjectValueNode|StringValueNode|VariableNode * * @api */ @@ -356,8 +356,6 @@ private function skip(string $kind): bool /** * If the next token is of the given kind, return that token after advancing * the parser. Otherwise, do not change the parser state and return false. - * - * @throws SyntaxError */ private function expect(string $kind): Token { @@ -379,8 +377,6 @@ private function expect(string $kind): Token /** * If the next token is a keyword with the given value, advance the lexer. * Otherwise, throw an error. - * - * @throws SyntaxError */ private function expectKeyword(string $value): void { @@ -425,8 +421,6 @@ private function unexpected(?Token $atToken = null): SyntaxError * and ends with a lex token of closeKind. Advances the parser * to the next lex token after the closing token. * - * @throws SyntaxError - * * @return NodeList */ private function any(string $openKind, callable $parseFn, string $closeKind): NodeList @@ -447,13 +441,11 @@ private function any(string $openKind, callable $parseFn, string $closeKind): No * and ends with a lex token of closeKind. Advances the parser * to the next lex token after the closing token. * - * @param callable(self): TNode $parseFn + * @template TNode of Node * - * @throws SyntaxError + * @param callable(self): TNode $parseFn * * @return NodeList - * - * @template TNode of Node */ private function many(string $openKind, callable $parseFn, string $closeKind): NodeList { @@ -469,8 +461,6 @@ private function many(string $openKind, callable $parseFn, string $closeKind): N /** * Converts a name lex token into a name parse node. - * - * @throws SyntaxError */ private function parseName(): NameNode { @@ -484,8 +474,6 @@ private function parseName(): NameNode /** * Implements the parsing rules in the Document section. - * - * @throws SyntaxError */ private function parseDocument(): DocumentNode { @@ -502,8 +490,6 @@ private function parseDocument(): DocumentNode } /** - * @throws SyntaxError - * * @return DefinitionNode&Node */ private function parseDefinition(): DefinitionNode @@ -540,8 +526,6 @@ private function parseDefinition(): DefinitionNode } /** - * @throws SyntaxError - * * @return ExecutableDefinitionNode&Node */ private function parseExecutableDefinition(): ExecutableDefinitionNode @@ -566,8 +550,6 @@ private function parseExecutableDefinition(): ExecutableDefinitionNode // Implements the parsing rules in the Operations section. /** - * @throws SyntaxError - * * @return OperationDefinitionNode&Node */ private function parseOperationDefinition(): OperationDefinitionNode @@ -601,9 +583,6 @@ private function parseOperationDefinition(): OperationDefinitionNode ]); } - /** - * @throws SyntaxError - */ private function parseOperationType(): string { $operationToken = $this->expect(Token::NAME); @@ -626,6 +605,7 @@ private function parseOperationType(): string */ private function parseVariableDefinitions(): NodeList { + // @phpstan-ignore-next-line generic type of empty NodeList is not initialized return $this->peek(Token::PAREN_L) ? $this->many( Token::PAREN_L, @@ -635,9 +615,6 @@ private function parseVariableDefinitions(): NodeList : new NodeList([]); } - /** - * @throws SyntaxError - */ private function parseVariableDefinition(): VariableDefinitionNode { $start = $this->lexer->token; @@ -657,9 +634,6 @@ private function parseVariableDefinition(): VariableDefinitionNode ]); } - /** - * @throws SyntaxError - */ private function parseVariable(): VariableNode { $start = $this->lexer->token; @@ -697,9 +671,6 @@ private function parseSelection(): SelectionNode : $this->parseField(); } - /** - * @throws SyntaxError - */ private function parseField(): FieldNode { $start = $this->lexer->token; @@ -724,8 +695,6 @@ private function parseField(): FieldNode } /** - * @throws SyntaxError - * * @return NodeList */ private function parseArguments(bool $isConst): NodeList @@ -734,14 +703,12 @@ private function parseArguments(bool $isConst): NodeList ? fn (): ArgumentNode => $this->parseConstArgument() : fn (): ArgumentNode => $this->parseArgument(); + // @phpstan-ignore-next-line generic type of empty NodeList is not initialized return $this->peek(Token::PAREN_L) ? $this->many(Token::PAREN_L, $parseFn, Token::PAREN_R) : new NodeList([]); } - /** - * @throws SyntaxError - */ private function parseArgument(): ArgumentNode { $start = $this->lexer->token; @@ -757,9 +724,6 @@ private function parseArgument(): ArgumentNode ]); } - /** - * @throws SyntaxError - */ private function parseConstArgument(): ArgumentNode { $start = $this->lexer->token; @@ -778,8 +742,6 @@ private function parseConstArgument(): ArgumentNode // Implements the parsing rules in the Fragments section. /** - * @throws SyntaxError - * * @return FragmentSpreadNode|InlineFragmentNode */ private function parseFragment(): SelectionNode @@ -804,9 +766,6 @@ private function parseFragment(): SelectionNode ]); } - /** - * @throws SyntaxError - */ private function parseFragmentDefinition(): FragmentDefinitionNode { $start = $this->lexer->token; @@ -834,9 +793,6 @@ private function parseFragmentDefinition(): FragmentDefinitionNode ]); } - /** - * @throws SyntaxError - */ private function parseFragmentName(): NameNode { if ('on' === $this->lexer->token->value) { @@ -866,8 +822,6 @@ private function parseFragmentName(): NameNode * * EnumValue : Name but not `true`, `false` or `null` * - * @throws SyntaxError - * * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|StringValueNode|VariableNode|ListValueNode|ObjectValueNode|NullValueNode */ private function parseValueLiteral(bool $isConst): ValueNode @@ -947,19 +901,11 @@ private function parseStringLiteral(): StringValueNode ]); } - /** - * @throws SyntaxError - * - * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|StringValueNode|VariableNode - */ private function parseConstValue(): ValueNode { return $this->parseValueLiteral(true); } - /** - * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|ObjectValueNode|StringValueNode|VariableNode - */ private function parseVariableValue(): ValueNode { return $this->parseValueLiteral(false); @@ -969,19 +915,13 @@ private function parseArray(bool $isConst): ListValueNode { $start = $this->lexer->token; $parseFn = $isConst - ? function () { - return $this->parseConstValue(); - } - : function () { - return $this->parseVariableValue(); - }; + ? fn (): ValueNode => $this->parseConstValue() + : fn (): ValueNode => $this->parseVariableValue(); - return new ListValueNode( - [ - 'values' => $this->any(Token::BRACKET_L, $parseFn, Token::BRACKET_R), - 'loc' => $this->loc($start), - ] - ); + return new ListValueNode([ + 'values' => $this->any(Token::BRACKET_L, $parseFn, Token::BRACKET_R), + 'loc' => $this->loc($start), + ]); } private function parseObject(bool $isConst): ObjectValueNode @@ -1016,8 +956,6 @@ private function parseObjectField(bool $isConst): ObjectFieldNode // Implements the parsing rules in the Directives section. /** - * @throws SyntaxError - * * @return NodeList */ private function parseDirectives(bool $isConst): NodeList @@ -1030,9 +968,6 @@ private function parseDirectives(bool $isConst): NodeList return new NodeList($directives); } - /** - * @throws SyntaxError - */ private function parseDirective(bool $isConst): DirectiveNode { $start = $this->lexer->token; @@ -1050,8 +985,6 @@ private function parseDirective(bool $isConst): DirectiveNode /** * Handles the Type: TypeName, ListType, and NonNullType parsing rules. * - * @throws SyntaxError - * * @return ListTypeNode|NamedTypeNode|NonNullTypeNode */ private function parseTypeReference(): TypeNode @@ -1092,8 +1025,6 @@ private function parseNamedType(): NamedTypeNode // Implements the parsing rules in the Type Definition section. /** - * @throws SyntaxError - * * @return TypeSystemDefinitionNode&Node */ private function parseTypeSystemDefinition(): TypeSystemDefinitionNode @@ -1151,9 +1082,6 @@ private function parseDescription(): ?StringValueNode return null; } - /** - * @throws SyntaxError - */ private function parseSchemaDefinition(): SchemaDefinitionNode { $start = $this->lexer->token; @@ -1175,9 +1103,6 @@ function (): OperationTypeDefinitionNode { ]); } - /** - * @throws SyntaxError - */ private function parseOperationTypeDefinition(): OperationTypeDefinitionNode { $start = $this->lexer->token; @@ -1192,9 +1117,6 @@ private function parseOperationTypeDefinition(): OperationTypeDefinitionNode ]); } - /** - * @throws SyntaxError - */ private function parseScalarTypeDefinition(): ScalarTypeDefinitionNode { $start = $this->lexer->token; @@ -1211,9 +1133,6 @@ private function parseScalarTypeDefinition(): ScalarTypeDefinitionNode ]); } - /** - * @throws SyntaxError - */ private function parseObjectTypeDefinition(): ObjectTypeDefinitionNode { $start = $this->lexer->token; @@ -1256,8 +1175,6 @@ private function parseImplementsInterfaces(): NodeList } /** - * @throws SyntaxError - * * @return NodeList */ private function parseFieldsDefinition(): NodeList @@ -1287,9 +1204,6 @@ private function parseFieldsDefinition(): NodeList return $nodeList; } - /** - * @throws SyntaxError - */ private function parseFieldDefinition(): FieldDefinitionNode { $start = $this->lexer->token; @@ -1311,27 +1225,20 @@ private function parseFieldDefinition(): FieldDefinitionNode } /** - * @throws SyntaxError - * * @return NodeList */ private function parseArgumentsDefinition(): NodeList { - /** @var NodeList $nodeList */ - $nodeList = $this->peek(Token::PAREN_L) + // @phpstan-ignore-next-line generic type of empty NodeList is not initialized + return $this->peek(Token::PAREN_L) ? $this->many( Token::PAREN_L, fn (): InputValueDefinitionNode => $this->parseInputValueDefinition(), Token::PAREN_R ) : new NodeList([]); - - return $nodeList; } - /** - * @throws SyntaxError - */ private function parseInputValueDefinition(): InputValueDefinitionNode { $start = $this->lexer->token; @@ -1356,9 +1263,6 @@ private function parseInputValueDefinition(): InputValueDefinitionNode ]); } - /** - * @throws SyntaxError - */ private function parseInterfaceTypeDefinition(): InterfaceTypeDefinitionNode { $start = $this->lexer->token; @@ -1382,8 +1286,6 @@ private function parseInterfaceTypeDefinition(): InterfaceTypeDefinitionNode /** * UnionTypeDefinition : * - Description? union Name Directives[Const]? UnionMemberTypes? - * - * @throws SyntaxError */ private function parseUnionTypeDefinition(): UnionTypeDefinitionNode { @@ -1420,9 +1322,6 @@ private function parseUnionMemberTypes(): NodeList return new NodeList($types); } - /** - * @throws SyntaxError - */ private function parseEnumTypeDefinition(): EnumTypeDefinitionNode { $start = $this->lexer->token; @@ -1442,27 +1341,20 @@ private function parseEnumTypeDefinition(): EnumTypeDefinitionNode } /** - * @throws SyntaxError - * * @return NodeList */ private function parseEnumValuesDefinition(): NodeList { - /** @var NodeList $nodeList */ - $nodeList = $this->peek(Token::BRACE_L) + // @phpstan-ignore-next-line generic type of empty NodeList is not initialized + return $this->peek(Token::BRACE_L) ? $this->many( Token::BRACE_L, fn (): EnumValueDefinitionNode => $this->parseEnumValueDefinition(), Token::BRACE_R ) : new NodeList([]); - - return $nodeList; } - /** - * @throws SyntaxError - */ private function parseEnumValueDefinition(): EnumValueDefinitionNode { $start = $this->lexer->token; @@ -1478,9 +1370,6 @@ private function parseEnumValueDefinition(): EnumValueDefinitionNode ]); } - /** - * @throws SyntaxError - */ private function parseInputObjectTypeDefinition(): InputObjectTypeDefinitionNode { $start = $this->lexer->token; @@ -1500,14 +1389,12 @@ private function parseInputObjectTypeDefinition(): InputObjectTypeDefinitionNode } /** - * @throws SyntaxError - * * @return NodeList */ private function parseInputFieldsDefinition(): NodeList { - /** @var NodeList $nodeList */ - $nodeList = $this->peek(Token::BRACE_L) + // @phpstan-ignore-next-line generic type of empty NodeList is not initialized + return $this->peek(Token::BRACE_L) ? $this->many( Token::BRACE_L, function (): InputValueDefinitionNode { @@ -1516,13 +1403,9 @@ function (): InputValueDefinitionNode { Token::BRACE_R ) : new NodeList([]); - - return $nodeList; } /** - * @throws SyntaxError - * * @return TypeExtensionNode&Node */ private function parseTypeExtension(): TypeExtensionNode @@ -1557,9 +1440,6 @@ private function parseTypeExtension(): TypeExtensionNode throw $this->unexpected($keywordToken); } - /** - * @throws SyntaxError - */ private function parseSchemaTypeExtension(): SchemaTypeExtensionNode { $start = $this->lexer->token; @@ -1585,9 +1465,6 @@ private function parseSchemaTypeExtension(): SchemaTypeExtensionNode ]); } - /** - * @throws SyntaxError - */ private function parseScalarTypeExtension(): ScalarTypeExtensionNode { $start = $this->lexer->token; @@ -1606,9 +1483,6 @@ private function parseScalarTypeExtension(): ScalarTypeExtensionNode ]); } - /** - * @throws SyntaxError - */ private function parseObjectTypeExtension(): ObjectTypeExtensionNode { $start = $this->lexer->token; @@ -1636,9 +1510,6 @@ private function parseObjectTypeExtension(): ObjectTypeExtensionNode ]); } - /** - * @throws SyntaxError - */ private function parseInterfaceTypeExtension(): InterfaceTypeExtensionNode { $start = $this->lexer->token; @@ -1669,8 +1540,6 @@ private function parseInterfaceTypeExtension(): InterfaceTypeExtensionNode * UnionTypeExtension : * - extend union Name Directives[Const]? UnionMemberTypes * - extend union Name Directives[Const]. - * - * @throws SyntaxError */ private function parseUnionTypeExtension(): UnionTypeExtensionNode { @@ -1692,9 +1561,6 @@ private function parseUnionTypeExtension(): UnionTypeExtensionNode ]); } - /** - * @throws SyntaxError - */ private function parseEnumTypeExtension(): EnumTypeExtensionNode { $start = $this->lexer->token; @@ -1718,9 +1584,6 @@ private function parseEnumTypeExtension(): EnumTypeExtensionNode ]); } - /** - * @throws SyntaxError - */ private function parseInputObjectTypeExtension(): InputObjectTypeExtensionNode { $start = $this->lexer->token; @@ -1747,8 +1610,6 @@ private function parseInputObjectTypeExtension(): InputObjectTypeExtensionNode /** * DirectiveDefinition : * - Description? directive @ Name ArgumentsDefinition? `repeatable`? on DirectiveLocations. - * - * @throws SyntaxError */ private function parseDirectiveDefinition(): DirectiveDefinitionNode { @@ -1773,8 +1634,6 @@ private function parseDirectiveDefinition(): DirectiveDefinitionNode } /** - * @throws SyntaxError - * * @return NodeList */ private function parseDirectiveLocations(): NodeList @@ -1789,9 +1648,6 @@ private function parseDirectiveLocations(): NodeList return new NodeList($locations); } - /** - * @throws SyntaxError - */ private function parseDirectiveLocation(): NameNode { $start = $this->lexer->token; diff --git a/src/Language/Printer.php b/src/Language/Printer.php index 4692ee916..8fc0a3fe8 100644 --- a/src/Language/Printer.php +++ b/src/Language/Printer.php @@ -213,6 +213,7 @@ protected function p(?Node $node, bool $isDescription = false): string return 'fragment ' . $this->p($node->name) . $this->wrap( '(', + // @phpstan-ignore-next-line generic type of empty NodeList is not recognized $this->printList($node->variableDefinitions ?? new NodeList([]), ', '), ')' ) @@ -408,7 +409,7 @@ protected function p(?Node $node, bool $isDescription = false): string return BlockString::print($node->value, $isDescription ? '' : ' '); } - return json_encode($node->value); + return json_encode($node->value, JSON_THROW_ON_ERROR); case $node instanceof UnionTypeDefinitionNode: $typesStr = $this->printList($node->types, ' | '); @@ -455,9 +456,9 @@ protected function p(?Node $node, bool $isDescription = false): string } /** - * @param NodeList $list - * * @template TNode of Node + * + * @param NodeList $list */ protected function printList(NodeList $list, string $separator = ''): string { @@ -472,9 +473,9 @@ protected function printList(NodeList $list, string $separator = ''): string /** * Print each item on its own line, wrapped in an indented "{ }" block. * - * @param NodeList $list - * * @template TNode of Node + * + * @param NodeList $list */ protected function printListBlock(NodeList $list): string { diff --git a/src/Language/Visitor.php b/src/Language/Visitor.php index 90e81866b..8ede7d2da 100644 --- a/src/Language/Visitor.php +++ b/src/Language/Visitor.php @@ -169,16 +169,16 @@ class Visitor /** * Visit the AST (see class description for details). * - * @param NodeList|Node $root - * @param VisitorArray $visitor + * @template TNode of Node + * + * @param NodeList|Node $root + * @param VisitorArray $visitor * @param array|null $keyMap * * @throws Exception * * @return Node|mixed * - * @template TNode of Node - * * @api */ public static function visit(object $root, array $visitor, ?array $keyMap = null) @@ -212,12 +212,8 @@ public static function visit(object $root, array $visitor, ?array $keyMap = null $parent = array_pop($ancestors); if ($isEdited) { - if ($inArray) { - // $node = $node; // arrays are value types in PHP - if ($node instanceof NodeList) { - $node = clone $node; - } - } else { + if ($node instanceof Node || $node instanceof NodeList) { + // TODO should we use cloneDeep()? $node = clone $node; } @@ -231,6 +227,7 @@ public static function visit(object $root, array $visitor, ?array $keyMap = null } if ($inArray && null === $editValue) { + assert($node instanceof NodeList, 'Follows from $inArray'); $node->splice($editKey, 1); ++$editOffset; } else { diff --git a/src/Server/Helper.php b/src/Server/Helper.php index e52618f06..f5e971c36 100644 --- a/src/Server/Helper.php +++ b/src/Server/Helper.php @@ -212,7 +212,7 @@ public function executeOperation(ServerConfig $config, OperationParams $op) * * @param array $operations * - * @return ExecutionResult|array|Promise + * @return array|Promise * * @api */ @@ -610,13 +610,10 @@ public function toPsrResponse($result, ResponseInterface $response, StreamInterf */ private function doConvertToPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream): ResponseInterface { - $httpStatus = $this->resolveHttpStatus($result); - - $result = json_encode($result); - $writableBodyStream->write($result); + $writableBodyStream->write(json_encode($result, JSON_THROW_ON_ERROR)); return $response - ->withStatus($httpStatus) + ->withStatus($this->resolveHttpStatus($result)) ->withHeader('Content-Type', 'application/json') ->withBody($writableBodyStream); } diff --git a/src/Server/ServerConfig.php b/src/Server/ServerConfig.php index b626942ef..62ca667dd 100644 --- a/src/Server/ServerConfig.php +++ b/src/Server/ServerConfig.php @@ -92,7 +92,7 @@ public static function create(array $config = []): self private ?Schema $schema = null; - /** @var mixed|callable(self, OperationParams, DocumentNode $doc): mixed|null */ + /** @var mixed|callable(self, OperationParams, DocumentNode): mixed|null */ private $context = null; /** diff --git a/src/Server/StandardServer.php b/src/Server/StandardServer.php index 2fe082361..1fab892a0 100644 --- a/src/Server/StandardServer.php +++ b/src/Server/StandardServer.php @@ -39,11 +39,9 @@ */ class StandardServer { - /** @var ServerConfig */ - private $config; + private ServerConfig $config; - /** @var Helper */ - private $helper; + private Helper $helper; /** * Converts and exception to error and sends spec-compliant HTTP 500 error. diff --git a/src/Type/Definition/AbstractType.php b/src/Type/Definition/AbstractType.php index 313804e7c..d85fee4c4 100644 --- a/src/Type/Definition/AbstractType.php +++ b/src/Type/Definition/AbstractType.php @@ -4,9 +4,10 @@ namespace GraphQL\Type\Definition; +use GraphQL\Deferred; + /** - * @phpstan-type AbstractTypeAlias InterfaceType|UnionType - * @phpstan-type ResolveTypeReturn ObjectType|string|callable(): (ObjectType|string|null)|null + * @phpstan-type ResolveTypeReturn ObjectType|string|callable(): (ObjectType|string|null)|Deferred|null * @phpstan-type ResolveType callable(mixed $objectValue, mixed $context, ResolveInfo $resolveInfo): ResolveTypeReturn */ interface AbstractType @@ -15,9 +16,9 @@ interface AbstractType * Resolves the concrete ObjectType for the given value. * * @param mixed $objectValue The resolved value for the object type - * @param mixed $context The context that was passed to GraphQL::execute() + * @param mixed $context The context that was passed to GraphQL::execute() * - * @return ObjectType|string|callable|null + * @return ObjectType|string|callable|Deferred|null * @phpstan-return ResolveTypeReturn */ public function resolveType($objectValue, $context, ResolveInfo $info); diff --git a/src/Type/Definition/Argument.php b/src/Type/Definition/Argument.php index 77decf5a1..a3c517621 100644 --- a/src/Type/Definition/Argument.php +++ b/src/Type/Definition/Argument.php @@ -12,7 +12,6 @@ use function is_array; /** - * @phpstan-import-type InputTypeAlias from InputType * @phpstan-type ArgumentType (Type&InputType)|callable(): (Type&InputType) * @phpstan-type UnnamedArgumentConfig array{ * name?: string, @@ -44,7 +43,7 @@ class Argument public ?InputValueDefinitionNode $astNode; - /** @var ArgumentConfig */ + /** @phpstan-var ArgumentConfig */ public array $config; /** @@ -83,7 +82,6 @@ public static function listFromConfig(iterable $config): array /** * @return Type&InputType - * @phpstan-return InputTypeAlias */ public function getType(): Type { @@ -107,7 +105,7 @@ public function isRequired(): bool } /** - * @param Type &NamedType $parentType + * @param Type&NamedType $parentType */ public function assertValid(FieldDefinition $parentField, Type $parentType): void { diff --git a/src/Type/Definition/BooleanType.php b/src/Type/Definition/BooleanType.php index bdb2945a4..e9eea85d9 100644 --- a/src/Type/Definition/BooleanType.php +++ b/src/Type/Definition/BooleanType.php @@ -7,6 +7,7 @@ use GraphQL\Error\Error; use GraphQL\Language\AST\BooleanValueNode; use GraphQL\Language\AST\Node; +use GraphQL\Language\Printer; use GraphQL\Utils\Utils; use function is_bool; @@ -33,7 +34,8 @@ public function parseValue($value): bool return $value; } - throw new Error('Boolean cannot represent a non boolean value: ' . Utils::printSafe($value)); + $notBoolean = Utils::printSafe($value); + throw new Error("Boolean cannot represent a non boolean value: {$notBoolean}"); } public function parseLiteral(Node $valueNode, ?array $variables = null): bool @@ -42,7 +44,7 @@ public function parseLiteral(Node $valueNode, ?array $variables = null): bool return $valueNode->value; } - // Intentionally without message, as all information already in wrapped Exception - throw new Error(); + $notBoolean = Printer::doPrint($valueNode); + throw new Error("Boolean cannot represent a non boolean value: {$notBoolean}", $valueNode); } } diff --git a/src/Type/Definition/CustomScalarType.php b/src/Type/Definition/CustomScalarType.php index 34cda69ff..ef5a26064 100644 --- a/src/Type/Definition/CustomScalarType.php +++ b/src/Type/Definition/CustomScalarType.php @@ -8,20 +8,30 @@ use GraphQL\Language\AST\Node; use GraphQL\Language\AST\ScalarTypeDefinitionNode; use GraphQL\Language\AST\ScalarTypeExtensionNode; +use GraphQL\Language\AST\ValueNode; use GraphQL\Utils\AST; use function is_callable; /** - * @phpstan-import-type LeafValueNode from LeafType - * @phpstan-type CustomScalarConfig array{ + * @phpstan-type InputCustomScalarConfig array{ + * name?: string|null, + * description?: string|null, + * serialize?: callable(mixed): mixed, + * parseValue: callable(mixed): mixed, + * parseLiteral: callable(ValueNode&Node, array|null): mixed, + * astNode?: ScalarTypeDefinitionNode|null, + * extensionASTNodes?: array|null, + * } + * @phpstan-type OutputCustomScalarConfig array{ * name?: string|null, * description?: string|null, * serialize: callable(mixed): mixed, * parseValue?: callable(mixed): mixed, - * parseLiteral?: callable(LeafValueNode, array|null): mixed, + * parseLiteral?: callable(ValueNode&Node, array|null): mixed, * astNode?: ScalarTypeDefinitionNode|null, * extensionASTNodes?: array|null, * } + * @phpstan-type CustomScalarConfig InputCustomScalarConfig|OutputCustomScalarConfig */ class CustomScalarType extends ScalarType { @@ -40,7 +50,11 @@ public function __construct(array $config) public function serialize($value) { - return $this->config['serialize']($value); + if (isset($this->config['serialize'])) { + return $this->config['serialize']($value); + } + + return $value; } public function parseValue($value) @@ -66,7 +80,7 @@ public function assertValid(): void parent::assertValid(); // @phpstan-ignore-next-line should not happen if used correctly - if (! isset($this->config['serialize']) || ! is_callable($this->config['serialize'])) { + if (isset($this->config['serialize']) && ! is_callable($this->config['serialize'])) { throw new InvariantViolation( "{$this->name} must provide \"serialize\" function. If this custom Scalar " . 'is also used as an input type, ensure "parseValue" and "parseLiteral" ' diff --git a/src/Type/Definition/EnumType.php b/src/Type/Definition/EnumType.php index 8ced10361..f3d1ff4ce 100644 --- a/src/Type/Definition/EnumType.php +++ b/src/Type/Definition/EnumType.php @@ -11,6 +11,7 @@ use GraphQL\Language\AST\EnumTypeExtensionNode; use GraphQL\Language\AST\EnumValueNode; use GraphQL\Language\AST\Node; +use GraphQL\Language\Printer; use GraphQL\Utils\MixedStore; use GraphQL\Utils\Utils; use function is_array; @@ -164,20 +165,26 @@ public function parseValue($value) public function parseLiteral(Node $valueNode, ?array $variables = null) { - if ($valueNode instanceof EnumValueNode) { - $name = $valueNode->value; + if (! $valueNode instanceof EnumValueNode) { + $valueStr = Printer::doPrint($valueNode); + throw new Error( + "Enum \"{$this->name}\" cannot represent non-enum value: {$valueStr}.{$this->didYouMean($valueStr)}", + $valueNode + ); + } - if (! isset($this->nameLookup)) { - $this->initializeNameLookup(); - } + $name = $valueNode->value; - if (isset($this->nameLookup[$name])) { - return $this->nameLookup[$name]->value; - } + if (! isset($this->nameLookup)) { + $this->initializeNameLookup(); } - // Intentionally without message, as all information already in wrapped Exception - throw new Error(); + if (isset($this->nameLookup[$name])) { + return $this->nameLookup[$name]->value; + } + + $valueStr = Printer::doPrint($valueNode); + throw new Error("Value \"{$valueStr}\" does not exist in \"{$this->name}\" enum.{$this->didYouMean($valueStr)}", $valueNode); } /** @@ -205,4 +212,19 @@ private function initializeNameLookup(): void $this->nameLookup[$value->name] = $value; } } + + protected function didYouMean(string $unknownValue): ?string + { + $suggestions = Utils::suggestionList( + $unknownValue, + array_map( + static fn (EnumValueDefinition $value): string => $value->name, + $this->getValues() + ) + ); + + return [] === $suggestions + ? null + : ' Did you mean the enum value ' . Utils::quotedOrList($suggestions) . '?'; + } } diff --git a/src/Type/Definition/EnumValueDefinition.php b/src/Type/Definition/EnumValueDefinition.php index 28c412319..115aeda27 100644 --- a/src/Type/Definition/EnumValueDefinition.php +++ b/src/Type/Definition/EnumValueDefinition.php @@ -28,11 +28,11 @@ class EnumValueDefinition public ?EnumValueDefinitionNode $astNode; - /** @var array */ - public $config; + /** @phpstan-var EnumValueConfig */ + public array $config; /** - * @param array $config + * @phpstan-param EnumValueConfig $config */ public function __construct(array $config) { diff --git a/src/Type/Definition/FieldDefinition.php b/src/Type/Definition/FieldDefinition.php index e134f97ad..d6cc5535b 100644 --- a/src/Type/Definition/FieldDefinition.php +++ b/src/Type/Definition/FieldDefinition.php @@ -18,7 +18,7 @@ * @phpstan-import-type FieldResolver from Executor * @phpstan-import-type ArgumentListConfig from Argument * @phpstan-type FieldType (Type&OutputType)|callable(): (Type&OutputType) - * @phpstan-type ComplexityFn callable(int, array): int|null + * @phpstan-type ComplexityFn callable(int, array): (int|null) * @phpstan-type FieldDefinitionConfig array{ * name: string, * type: FieldType, @@ -38,7 +38,16 @@ * astNode?: FieldDefinitionNode|null, * complexity?: ComplexityFn|null, * } - * @phpstan-type FieldMapConfig (callable(): iterable)|iterable + * @phpstan-type FieldsConfig iterable|(callable(): iterable) + */ +/* + * TODO check if newer versions of PHPStan can handle the full definition, it currently crashes when it is used + * @phpstan-type EagerListEntry FieldDefinitionConfig|(Type&OutputType) + * @phpstan-type EagerMapEntry UnnamedFieldDefinitionConfig|FieldDefinition + * @phpstan-type FieldsList iterable + * @phpstan-type FieldsMap iterable + * @phpstan-type FieldsIterable FieldsList|FieldsMap + * @phpstan-type FieldsConfig FieldsIterable|(callable(): FieldsIterable) */ class FieldDefinition { @@ -61,13 +70,16 @@ class FieldDefinition public ?FieldDefinitionNode $astNode; - /** @var ComplexityFn|null */ + /** + * @var callable|null + * @phpstan-var ComplexityFn|null + */ public $complexityFn; /** * Original field definition config. * - * @var FieldDefinitionConfig + * @phpstan-var FieldDefinitionConfig */ public array $config; @@ -77,7 +89,7 @@ class FieldDefinition /** * @param FieldDefinitionConfig $config */ - protected function __construct(array $config) + public function __construct(array $config) { $this->name = $config['name']; $this->resolveFn = $config['resolve'] ?? null; @@ -94,10 +106,10 @@ protected function __construct(array $config) /** * @param ObjectType|InterfaceType $parentType - * @param callable|iterable $fields - * @phpstan-param FieldMapConfig $fields + * @param callable|iterable $fields + * @phpstan-param FieldsConfig $fields * - * @return array + * @return array */ public static function defineFieldMap(Type $parentType, $fields): array { @@ -125,13 +137,8 @@ public static function defineFieldMap(Type $parentType, $fields): array $field['name'] = $maybeName; } - if (isset($field['args']) && ! is_array($field['args'])) { - throw new InvariantViolation( - "{$parentType->name}.{$maybeName} args must be an array." - ); - } - - $fieldDef = self::create($field); + // @phpstan-ignore-next-line PHPStan won't let us define the whole type + $fieldDef = new self($field); } elseif ($field instanceof self) { $fieldDef = $field; } elseif (is_callable($field)) { @@ -141,9 +148,13 @@ public static function defineFieldMap(Type $parentType, $fields): array ); } - $fieldDef = new UnresolvedFieldDefinition($parentType, $maybeName, $field); + $fieldDef = new UnresolvedFieldDefinition($maybeName, $field); } elseif ($field instanceof Type) { - $fieldDef = self::create(['name' => $maybeName, 'type' => $field]); + // @phpstan-ignore-next-line PHPStan won't let us define the whole type + $fieldDef = new self([ + 'name' => $maybeName, + 'type' => $field, + ]); } else { $invalidFieldConfig = Utils::printSafe($field); @@ -158,14 +169,6 @@ public static function defineFieldMap(Type $parentType, $fields): array return $map; } - /** - * @param array $field - */ - public static function create(array $field): FieldDefinition - { - return new self($field); - } - public function getArg(string $name): ?Argument { foreach ($this->args as $arg) { @@ -196,7 +199,7 @@ public function isDeprecated(): bool } /** - * @param Type &NamedType $parentType + * @param Type&NamedType $parentType * * @throws InvariantViolation */ diff --git a/src/Type/Definition/FloatType.php b/src/Type/Definition/FloatType.php index c52fb0bb3..6b5f03b8f 100644 --- a/src/Type/Definition/FloatType.php +++ b/src/Type/Definition/FloatType.php @@ -9,6 +9,7 @@ use GraphQL\Language\AST\FloatValueNode; use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\Node; +use GraphQL\Language\Printer; use GraphQL\Utils\Utils; use function is_bool; use function is_finite; @@ -48,10 +49,8 @@ public function parseValue($value): float : null; if (null === $float || ! is_finite($float)) { - throw new Error( - 'Float cannot represent non numeric value: ' - . Utils::printSafe($value) - ); + $notFloat = Utils::printSafe($value); + throw new Error("Float cannot represent non numeric value: {$notFloat}"); } return $float; @@ -63,7 +62,7 @@ public function parseLiteral(Node $valueNode, ?array $variables = null) return (float) $valueNode->value; } - // Intentionally without message, as all information already in wrapped Exception - throw new Error(); + $notFloat = Printer::doPrint($valueNode); + throw new Error("Float cannot represent non numeric value: {$notFloat}", $valueNode); } } diff --git a/src/Type/Definition/HasFieldsTypeImplementation.php b/src/Type/Definition/HasFieldsTypeImplementation.php index 4d80c58f6..a1019d826 100644 --- a/src/Type/Definition/HasFieldsTypeImplementation.php +++ b/src/Type/Definition/HasFieldsTypeImplementation.php @@ -43,11 +43,12 @@ public function findField(string $name): ?FieldDefinition return null; } - if ($this->fields[$name] instanceof UnresolvedFieldDefinition) { - $this->fields[$name] = $this->fields[$name]->resolve(); + $field = $this->fields[$name]; + if ($field instanceof UnresolvedFieldDefinition) { + return $this->fields[$name] = $field->resolve(); } - return $this->fields[$name]; + return $field; } public function hasField(string $name): bool @@ -62,13 +63,12 @@ public function getFields(): array $this->initializeFields(); foreach ($this->fields as $name => $field) { - if (! ($field instanceof UnresolvedFieldDefinition)) { - continue; + if ($field instanceof UnresolvedFieldDefinition) { + $this->fields[$name] = $field->resolve(); } - - $this->fields[$name] = $field->resolve(); } + // @phpstan-ignore-next-line all field definitions are now resolved return $this->fields; } diff --git a/src/Type/Definition/IDType.php b/src/Type/Definition/IDType.php index 5daf010e2..7c85d5134 100644 --- a/src/Type/Definition/IDType.php +++ b/src/Type/Definition/IDType.php @@ -9,6 +9,7 @@ use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\StringValueNode; +use GraphQL\Language\Printer; use GraphQL\Utils\Utils; use function is_int; use function is_object; @@ -33,7 +34,8 @@ public function serialize($value): string || (is_object($value) && method_exists($value, '__toString')); if (! $canCast) { - throw new SerializationError('ID cannot represent value: ' . Utils::printSafe($value)); + $notID = Utils::printSafe($value); + throw new SerializationError("ID cannot represent a non-string and non-integer value: {$notID}"); } return (string) $value; @@ -45,7 +47,8 @@ public function parseValue($value): string return (string) $value; } - throw new Error('ID cannot represent value: ' . Utils::printSafe($value)); + $notID = Utils::printSafe($value); + throw new Error("ID cannot represent a non-string and non-integer value: {$notID}"); } public function parseLiteral(Node $valueNode, ?array $variables = null): string @@ -54,7 +57,7 @@ public function parseLiteral(Node $valueNode, ?array $variables = null): string return $valueNode->value; } - // Intentionally without message, as the wrapping Exception will have all necessary information - throw new Error(); + $notID = Printer::doPrint($valueNode); + throw new Error("ID cannot represent a non-string and non-integer value: {$notID}", $valueNode); } } diff --git a/src/Type/Definition/InputObjectField.php b/src/Type/Definition/InputObjectField.php index 6cd79d322..9bb70c3f9 100644 --- a/src/Type/Definition/InputObjectField.php +++ b/src/Type/Definition/InputObjectField.php @@ -11,7 +11,6 @@ use GraphQL\Utils\Utils; /** - * @phpstan-import-type InputTypeAlias from InputType * @phpstan-type ArgumentType (Type&InputType)|callable(): (Type&InputType) * @phpstan-type InputObjectFieldConfig array{ * name: string, @@ -42,7 +41,7 @@ class InputObjectField public ?InputValueDefinitionNode $astNode; - /** @var array */ + /** @phpstan-var InputObjectFieldConfig */ public array $config; /** @@ -61,7 +60,6 @@ public function __construct(array $config) /** * @return Type&InputType - * @phpstan-return InputTypeAlias */ public function getType(): Type { @@ -85,7 +83,7 @@ public function isRequired(): bool } /** - * @param Type &NamedType $parentType + * @param Type&NamedType $parentType * * @throws InvariantViolation */ @@ -104,6 +102,7 @@ public function assertValid(Type $parentType): void throw new InvariantViolation("{$parentType->name}.{$this->name} field type must be Input Type but got: {$notInputType}"); } + // @phpstan-ignore-next-line should not happen if used properly if (array_key_exists('resolve', $this->config)) { throw new InvariantViolation("{$parentType->name}.{$this->name} field has a resolve property, but Input Types cannot define resolvers."); } diff --git a/src/Type/Definition/InputObjectType.php b/src/Type/Definition/InputObjectType.php index d78388855..7fb07abeb 100644 --- a/src/Type/Definition/InputObjectType.php +++ b/src/Type/Definition/InputObjectType.php @@ -131,9 +131,9 @@ protected function initializeField($nameOrIndex, $field): void } if (is_array($field)) { - $name = $field['name'] ??= $nameOrIndex; + $field['name'] ??= $nameOrIndex; - if (! is_string($name)) { + if (! is_string($field['name'])) { throw new InvariantViolation( "{$this->name} fields must be an associative array with field names as keys, an array of arrays with a name attribute, or a callable which returns one of those." ); diff --git a/src/Type/Definition/InputType.php b/src/Type/Definition/InputType.php index 6b10be4f3..d970bff3d 100644 --- a/src/Type/Definition/InputType.php +++ b/src/Type/Definition/InputType.php @@ -16,8 +16,6 @@ | InputObjectType | ListOfType, >; - * - * @phpstan-type InputTypeAlias ScalarType|EnumType|InputObjectType|ListOfType|NonNull */ interface InputType { diff --git a/src/Type/Definition/IntType.php b/src/Type/Definition/IntType.php index 96cff30bd..ba138582d 100644 --- a/src/Type/Definition/IntType.php +++ b/src/Type/Definition/IntType.php @@ -9,6 +9,7 @@ use GraphQL\Error\SerializationError; use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\Node; +use GraphQL\Language\Printer; use GraphQL\Utils\Utils; use function is_bool; use function is_float; @@ -43,17 +44,13 @@ public function serialize($value): int : null; if (null === $float || floor($float) !== $float) { - throw new SerializationError( - 'Int cannot represent non-integer value: ' - . Utils::printSafe($value) - ); + $notInt = Utils::printSafe($value); + throw new SerializationError("Int cannot represent non-integer value: {$notInt}"); } if ($float > self::MAX_INT || $float < self::MIN_INT) { - throw new SerializationError( - 'Int cannot represent non 32-bit signed integer value: ' - . Utils::printSafe($value) - ); + $outOfRangeInt = Utils::printSafe($value); + throw new SerializationError("Int cannot represent non 32-bit signed integer value: {$outOfRangeInt}"); } return (int) $float; @@ -61,20 +58,17 @@ public function serialize($value): int public function parseValue($value): int { - $isInt = is_int($value) || (is_float($value) && floor($value) === $value); + $isInt = is_int($value) + || (is_float($value) && floor($value) === $value); if (! $isInt) { - throw new Error( - 'Int cannot represent non-integer value: ' - . Utils::printSafe($value) - ); + $notInt = Utils::printSafe($value); + throw new Error("Int cannot represent non-integer value: {$notInt}"); } if ($value > self::MAX_INT || $value < self::MIN_INT) { - throw new Error( - 'Int cannot represent non 32-bit signed integer value: ' - . Utils::printSafe($value) - ); + $outOfRangeInt = Utils::printSafe($value); + throw new Error("Int cannot represent non 32-bit signed integer value: {$outOfRangeInt}"); } return (int) $value; @@ -89,7 +83,7 @@ public function parseLiteral(Node $valueNode, ?array $variables = null): int } } - // Intentionally without message, as all information already in wrapped Exception - throw new Error(); + $notInt = Printer::doPrint($valueNode); + throw new Error("Int cannot represent non-integer value: {$notInt}", $valueNode); } } diff --git a/src/Type/Definition/InterfaceType.php b/src/Type/Definition/InterfaceType.php index f3700684d..47580ecac 100644 --- a/src/Type/Definition/InterfaceType.php +++ b/src/Type/Definition/InterfaceType.php @@ -13,12 +13,12 @@ /** * @phpstan-import-type ResolveType from AbstractType - * @phpstan-import-type FieldMapConfig from FieldDefinition + * @phpstan-import-type FieldsConfig from FieldDefinition * @phpstan-type InterfaceTypeReference InterfaceType|callable(): InterfaceType * @phpstan-type InterfaceConfig array{ * name?: string|null, * description?: string|null, - * fields: FieldMapConfig, + * fields: FieldsConfig, * interfaces?: iterable|callable(): iterable, * resolveType?: ResolveType|null, * astNode?: InterfaceTypeDefinitionNode|null, diff --git a/src/Type/Definition/LeafType.php b/src/Type/Definition/LeafType.php index 6583f701e..d26689a70 100644 --- a/src/Type/Definition/LeafType.php +++ b/src/Type/Definition/LeafType.php @@ -4,13 +4,8 @@ namespace GraphQL\Type\Definition; -use GraphQL\Language\AST\BooleanValueNode; -use GraphQL\Language\AST\EnumValueNode; -use GraphQL\Language\AST\FloatValueNode; -use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\Node; -use GraphQL\Language\AST\NullValueNode; -use GraphQL\Language\AST\StringValueNode; +use GraphQL\Language\AST\ValueNode; /* export type GraphQLLeafType = @@ -18,9 +13,6 @@ GraphQLEnumType; */ -/** - * @phpstan-type LeafValueNode IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|NullValueNode|EnumValueNode - */ interface LeafType { /** @@ -50,8 +42,8 @@ public function parseValue($value); * * Should throw an exception with a client friendly message on invalid value nodes, @see ClientAware. * + * @param ValueNode&Node $valueNode * @param array|null $variables - * @phpstan-param LeafValueNode $valueNode * * @return mixed */ diff --git a/src/Type/Definition/ListOfType.php b/src/Type/Definition/ListOfType.php index e329ca0e9..78a10b244 100644 --- a/src/Type/Definition/ListOfType.php +++ b/src/Type/Definition/ListOfType.php @@ -46,7 +46,8 @@ public function getInnermostType(): NamedType $type = $type->getWrappedType(); } - /** @var Type&NamedType $type known because we unwrapped all the way down */ + assert($type instanceof NamedType, 'known because we unwrapped all the way down'); + return $type; } } diff --git a/src/Type/Definition/NonNull.php b/src/Type/Definition/NonNull.php index e1c54c618..a0ccdedc0 100644 --- a/src/Type/Definition/NonNull.php +++ b/src/Type/Definition/NonNull.php @@ -36,7 +36,6 @@ public function toString(): string */ public function getWrappedType(): Type { - // @phpstan-ignore-next-line generics in Schema::resolveType() are not recognized correctly return Schema::resolveType($this->wrappedType); } @@ -47,7 +46,8 @@ public function getInnermostType(): NamedType $type = $type->getWrappedType(); } - /** @var Type&NamedType $type known because we unwrapped all the way down */ + assert($type instanceof NamedType, 'known because we unwrapped all the way down'); + return $type; } } diff --git a/src/Type/Definition/ObjectType.php b/src/Type/Definition/ObjectType.php index e39d97098..4ae135252 100644 --- a/src/Type/Definition/ObjectType.php +++ b/src/Type/Definition/ObjectType.php @@ -57,7 +57,7 @@ * @phpstan-type ObjectConfig array{ * name?: string|null, * description?: string|null, - * resolveField?: FieldResolver, + * resolveField?: FieldResolver|null, * fields: (callable(): iterable)|iterable, * interfaces?: iterable|callable(): iterable, * isTypeOf?: (callable(mixed $objectValue, mixed $context, ResolveInfo $resolveInfo): (bool|Deferred|null))|null, @@ -76,7 +76,10 @@ class ObjectType extends Type implements OutputType, CompositeType, NullableType /** @var array */ public array $extensionASTNodes; - /** @var callable|null */ + /** + * @var callable|null + * @phpstan-var FieldResolver|null + */ public $resolveFieldFn; /** @phpstan-var ObjectConfig */ diff --git a/src/Type/Definition/QueryPlan.php b/src/Type/Definition/QueryPlan.php index 5e5c8a3fb..8821c5eb9 100644 --- a/src/Type/Definition/QueryPlan.php +++ b/src/Type/Definition/QueryPlan.php @@ -26,6 +26,11 @@ use function is_array; use function is_numeric; +/** + * @phpstan-type QueryPlanOptions array{ + * groupImplementorFields?: bool, + * } + */ class QueryPlan { /** @var array> */ @@ -45,17 +50,17 @@ class QueryPlan private bool $groupImplementorFields; /** - * @param iterable $fieldNodes - * @param array $variableValues + * @param iterable $fieldNodes + * @param array $variableValues * @param array $fragments - * @param array $options TODO move to using key + * @param QueryPlanOptions $options */ public function __construct(ObjectType $parentType, Schema $schema, iterable $fieldNodes, array $variableValues, array $fragments, array $options = []) { $this->schema = $schema; $this->variableValues = $variableValues; $this->fragments = $fragments; - $this->groupImplementorFields = in_array('group-implementor-fields', $options, true); + $this->groupImplementorFields = $options['groupImplementorFields'] ?? false; $this->analyzeQueryPlan($parentType, $fieldNodes); } @@ -113,16 +118,15 @@ private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes): { $queryPlan = []; $implementors = []; - /** @var FieldNode $fieldNode */ foreach ($fieldNodes as $fieldNode) { if (null === $fieldNode->selectionSet) { continue; } - /** @var ObjectType|InterfaceType $type proven because it must be a type with fields and was unwrapped */ $type = Type::getNamedType( $parentType->getField($fieldNode->name->value)->getType() ); + assert($type instanceof ObjectType || $type instanceof InterfaceType, 'proven because it must be a type with fields and was unwrapped'); $subfields = $this->analyzeSelectionSet($fieldNode->selectionSet, $type, $implementors); @@ -149,8 +153,8 @@ private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes): } /** - * @param InterfaceType|ObjectType $parentType - * @param array $implementors + * @param Type&NamedType $parentType + * @param array $implementors * * @throws Error * @@ -162,6 +166,8 @@ private function analyzeSelectionSet(SelectionSetNode $selectionSet, Type $paren $implementors = []; foreach ($selectionSet->selections as $selectionNode) { if ($selectionNode instanceof FieldNode) { + assert($parentType instanceof HasFieldsType, 'ensured by query validation'); + $fieldName = $selectionNode->name->value; if (Introspection::TYPE_NAME_FIELD_NAME === $fieldName) { @@ -225,10 +231,10 @@ private function analyzeSubFields(Type $type, SelectionSetNode $selectionSet, ar } /** - * @param Type &NamedType $parentType - * @param Type &NamedType $type - * @param array $fields - * @param array $subfields + * @param Type&NamedType $parentType + * @param Type&NamedType $type + * @param array $fields + * @param array $subfields * @param array $implementors * * @return array diff --git a/src/Type/Definition/ResolveInfo.php b/src/Type/Definition/ResolveInfo.php index 3bdcfbd86..061a9082d 100644 --- a/src/Type/Definition/ResolveInfo.php +++ b/src/Type/Definition/ResolveInfo.php @@ -17,6 +17,8 @@ * Structure containing information useful for field resolution process. * * Passed as 4th argument to every field resolver. See [docs on field resolving (data fetching)](data-fetching.md). + * + * @phpstan-import-type QueryPlanOptions from QueryPlan */ class ResolveInfo { @@ -184,7 +186,6 @@ public function getFieldSelection(int $depth = 0): array { $fields = []; - /** @var FieldNode $fieldNode */ foreach ($this->fieldNodes as $fieldNode) { if (null === $fieldNode->selectionSet) { continue; @@ -200,7 +201,7 @@ public function getFieldSelection(int $depth = 0): array } /** - * @param mixed[] $options + * @param QueryPlanOptions $options */ public function lookAhead(array $options = []): QueryPlan { @@ -232,7 +233,6 @@ private function foldSelectionSet(SelectionSetNode $selectionSet, int $descend): } elseif ($selectionNode instanceof FragmentSpreadNode) { $spreadName = $selectionNode->name->value; if (isset($this->fragments[$spreadName])) { - /** @var FragmentDefinitionNode $fragment */ $fragment = $this->fragments[$spreadName]; $fields = array_merge_recursive( $this->foldSelectionSet($fragment->selectionSet, $descend), diff --git a/src/Type/Definition/StringType.php b/src/Type/Definition/StringType.php index 5ca460360..f45ec6a18 100644 --- a/src/Type/Definition/StringType.php +++ b/src/Type/Definition/StringType.php @@ -8,6 +8,7 @@ use GraphQL\Error\SerializationError; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\StringValueNode; +use GraphQL\Language\Printer; use GraphQL\Utils\Utils; use function is_object; use function is_scalar; @@ -41,9 +42,8 @@ public function serialize($value): string public function parseValue($value): string { if (! is_string($value)) { - throw new Error( - 'String cannot represent a non string value: ' . Utils::printSafe($value) - ); + $notString = Utils::printSafe($value); + throw new Error("String cannot represent a non string value: {$notString}"); } return $value; @@ -55,7 +55,7 @@ public function parseLiteral(Node $valueNode, ?array $variables = null): string return $valueNode->value; } - // Intentionally without message, as all information already in wrapped Exception - throw new Error(); + $notString = Printer::doPrint($valueNode); + throw new Error("String cannot represent a non string value: {$notString}", $valueNode); } } diff --git a/src/Type/Definition/Type.php b/src/Type/Definition/Type.php index 7679ac219..c94bb7ede 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -91,12 +91,12 @@ public static function float(): ScalarType } /** + * @template T of Type + * * @param T|callable():T $type * * @return ListOfType * - * @template T of Type - * * @api */ public static function listOf($type): ListOfType @@ -190,10 +190,13 @@ public static function isInputType($type): bool */ public static function getNamedType(?Type $type): ?Type { - /** @var (Type&WrappingType)|(Type&NamedType)|null $type */ - return $type instanceof WrappingType - ? $type->getInnermostType() - : $type; + if ($type instanceof WrappingType) { + return $type->getInnermostType(); + } + + assert(null === $type || $type instanceof NamedType, 'only other option'); + + return $type; } /** @@ -243,10 +246,13 @@ public static function isAbstractType($type): bool */ public static function getNullableType(Type $type): Type { - /** @var (Type&NullableType)|(Type&NonNull) $type */ - return $type instanceof NonNull - ? $type->getWrappedType() - : $type; + if ($type instanceof NonNull) { + return $type->getWrappedType(); + } + + assert($type instanceof NullableType, 'only other option'); + + return $type; } abstract public function toString(): string; diff --git a/src/Type/Definition/UnresolvedFieldDefinition.php b/src/Type/Definition/UnresolvedFieldDefinition.php index 6e354fbcf..7ebeb2142 100644 --- a/src/Type/Definition/UnresolvedFieldDefinition.php +++ b/src/Type/Definition/UnresolvedFieldDefinition.php @@ -4,28 +4,27 @@ namespace GraphQL\Type\Definition; -use GraphQL\Error\InvariantViolation; -use function is_array; - +/** + * @phpstan-import-type UnnamedFieldDefinitionConfig from FieldDefinition + * @phpstan-type DefinitionResolver callable(): (FieldDefinition|(Type&OutputType)|UnnamedFieldDefinitionConfig) + */ class UnresolvedFieldDefinition { - /** @var ObjectType|InterfaceType */ - private Type $parentType; - private string $name; - /** @var callable(): (FieldDefinition|array|Type) */ - private $resolver; + /** + * @var callable + * @phpstan-var DefinitionResolver + */ + private $definitionResolver; /** - * @param ObjectType|InterfaceType $parentType - * @param callable(): (FieldDefinition|array|Type) $resolver + * @param DefinitionResolver $definitionResolver */ - public function __construct(Type $parentType, string $name, callable $resolver) + public function __construct(string $name, callable $definitionResolver) { - $this->parentType = $parentType; $this->name = $name; - $this->resolver = $resolver; + $this->definitionResolver = $definitionResolver; } public function getName(): string @@ -35,36 +34,19 @@ public function getName(): string public function resolve(): FieldDefinition { - $field = ($this->resolver)(); + $field = ($this->definitionResolver)(); if ($field instanceof FieldDefinition) { - if ($field->name !== $this->name) { - throw new InvariantViolation( - "{$this->parentType->name}.{$this->name} should not dynamically change its name when resolved lazily." - ); - } - return $field; } - if (! is_array($field)) { - return FieldDefinition::create(['name' => $this->name, 'type' => $field]); - } - - if (! isset($field['name'])) { - $field['name'] = $this->name; - } elseif ($field['name'] !== $this->name) { - throw new InvariantViolation( - "{$this->parentType->name}.{$this->name} should not dynamically change its name when resolved lazily." - ); - } - - if (isset($field['args']) && ! is_array($field['args'])) { - throw new InvariantViolation( - "{$this->parentType->name}.{$this->name} args must be an array." - ); + if ($field instanceof Type) { + return new FieldDefinition([ + 'name' => $this->name, + 'type' => $field, + ]); } - return FieldDefinition::create($field); + return new FieldDefinition($field + ['name' => $this->name]); } } diff --git a/src/Type/Introspection.php b/src/Type/Introspection.php index 4634d9958..d08ff9c5d 100644 --- a/src/Type/Introspection.php +++ b/src/Type/Introspection.php @@ -160,7 +160,7 @@ enumValues(includeDeprecated: true) { } /** - * @param Type &NamedType $type + * @param Type&NamedType $type */ public static function isIntrospectionType(NamedType $type): bool { @@ -676,7 +676,7 @@ public static function _directiveLocation(): EnumType public static function schemaMetaFieldDef(): FieldDefinition { - return self::$map[self::SCHEMA_FIELD_NAME] ??= FieldDefinition::create([ + return self::$map[self::SCHEMA_FIELD_NAME] ??= new FieldDefinition([ 'name' => self::SCHEMA_FIELD_NAME, 'type' => Type::nonNull(self::_schema()), 'description' => 'Access the current type schema of this server.', @@ -687,7 +687,7 @@ public static function schemaMetaFieldDef(): FieldDefinition public static function typeMetaFieldDef(): FieldDefinition { - return self::$map[self::TYPE_FIELD_NAME] ??= FieldDefinition::create([ + return self::$map[self::TYPE_FIELD_NAME] ??= new FieldDefinition([ 'name' => self::TYPE_FIELD_NAME, 'type' => self::_type(), 'description' => 'Request the type information of a single type.', @@ -703,7 +703,7 @@ public static function typeMetaFieldDef(): FieldDefinition public static function typeNameMetaFieldDef(): FieldDefinition { - return self::$map[self::TYPE_NAME_FIELD_NAME] ??= FieldDefinition::create([ + return self::$map[self::TYPE_NAME_FIELD_NAME] ??= new FieldDefinition([ 'name' => self::TYPE_NAME_FIELD_NAME, 'type' => Type::nonNull(Type::string()), 'description' => 'The name of the current Object type at runtime.', diff --git a/src/Type/Schema.php b/src/Type/Schema.php index c3df54581..cffe10c1e 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -5,7 +5,6 @@ namespace GraphQL\Type; use Generator; -use function get_class; use GraphQL\Error\Error; use GraphQL\Error\InvariantViolation; use GraphQL\GraphQL; @@ -23,7 +22,6 @@ use GraphQL\Utils\TypeInfo; use GraphQL\Utils\Utils; use function implode; -use InvalidArgumentException; use function is_array; use function is_callable; use function is_iterable; @@ -189,11 +187,11 @@ public function getTypeMap(): array } /** - * @return array + * @return array */ private function collectAllTypes(): array { - /** @var array $typeMap */ + /** @var array $typeMap */ $typeMap = []; foreach ($this->resolvedTypes as $type) { TypeInfo::extractTypes($type, $typeMap); @@ -368,12 +366,12 @@ private function defaultTypeLoader(string $typeName): ?Type } /** + * @template T of Type + * * @param Type|callable $type * @phpstan-param T|callable():T $type * * @phpstan-return T - * - * @template T of Type */ public static function resolveType($type): Type { @@ -390,17 +388,21 @@ public static function resolveType($type): Type * * This operation requires full schema scan. Do not use in production environment. * - * @param InterfaceType|UnionType $abstractType + * @param AbstractType&Type $abstractType * * @return array * * @api */ - public function getPossibleTypes(Type $abstractType): array + public function getPossibleTypes(AbstractType $abstractType): array { - return $abstractType instanceof UnionType - ? $abstractType->getTypes() - : $this->getImplementations($abstractType)->objects(); + if ($abstractType instanceof UnionType) { + return $abstractType->getTypes(); + } + + assert($abstractType instanceof InterfaceType, 'only other option'); + + return $this->getImplementations($abstractType)->objects(); } /** @@ -423,7 +425,15 @@ private function collectImplementations(): array if (! isset($this->implementationsMap)) { $this->implementationsMap = []; - /** @var array> $foundImplementations */ + /** + * @var array< + * string, + * array{ + * objects: array, + * interfaces: array, + * } + * > $foundImplementations + */ $foundImplementations = []; foreach ($this->getTypeMap() as $type) { if ($type instanceof InterfaceType) { @@ -460,8 +470,8 @@ private function collectImplementations(): array /** * Returns true if the given type is a sub type of the given abstract type. * - * @param UnionType|InterfaceType $abstractType - * @param ObjectType|InterfaceType $maybeSubType + * @param AbstractType&Type $abstractType + * @param ImplementingType&Type $maybeSubType * * @api */ @@ -471,13 +481,9 @@ public function isSubType(AbstractType $abstractType, ImplementingType $maybeSub return $maybeSubType->implementsInterface($abstractType); } - // @phpstan-ignore-next-line necessary until function can be type hinted with actual union type - if ($abstractType instanceof UnionType) { - return $abstractType->isPossibleType($maybeSubType); - } + assert($abstractType instanceof UnionType, 'only other option'); - // @phpstan-ignore-next-line necessary until function can be type hinted with actual union type - throw new InvalidArgumentException('$abstractType must be of type UnionType|InterfaceType got: ' . get_class($abstractType) . '.'); + return $abstractType->isPossibleType($maybeSubType); } /** diff --git a/src/Type/SchemaValidationContext.php b/src/Type/SchemaValidationContext.php index 36d24c9af..aa9f69bd6 100644 --- a/src/Type/SchemaValidationContext.php +++ b/src/Type/SchemaValidationContext.php @@ -92,7 +92,7 @@ public function validateRootTypes(): void } /** - * @param array|Node|null $nodes + * @param array|Node|null $nodes */ public function reportError(string $message, $nodes = null): void { @@ -165,7 +165,7 @@ public function validateDirectiveDefinitions(): void $argNames[$argName] = true; // Ensure the type is an input type. - // @phpstan-ignore-next-line the type of $arg->getType() says it is an input type, but it might not always be true + // @phpstan-ignore-next-line necessary until PHP supports union types if (Type::isInputType($arg->getType())) { continue; } @@ -200,7 +200,7 @@ public function validateDirectiveDefinitions(): void } /** - * @param Type|Directive|FieldDefinition|EnumValueDefinition|InputObjectField|Argument $object + * @param (Type&NamedType)|Directive|FieldDefinition|EnumValueDefinition|InputObjectField|Argument $object */ private function validateName(object $object): void { @@ -208,7 +208,7 @@ private function validateName(object $object): void $error = Utils::isValidNameError($object->name, $object->astNode); if ( null === $error - || ($object instanceof Type && $object instanceof NamedType && Introspection::isIntrospectionType($object)) + || ($object instanceof Type && Introspection::isIntrospectionType($object)) ) { return; } @@ -565,13 +565,11 @@ private function getAllFieldArgNodes(Type $type, string $fieldName, string $argN { $argNodes = []; $fieldNode = $this->getFieldNode($type, $fieldName); - if (null !== $fieldNode && null !== $fieldNode->arguments) { + if (null !== $fieldNode) { foreach ($fieldNode->arguments as $node) { - if ($node->name->value !== $argName) { - continue; + if ($node->name->value === $argName) { + $argNodes[] = $node; } - - $argNodes[] = $node; } } @@ -643,13 +641,19 @@ private function validateInterfaces(ImplementingType $type): void } /** - * @param Schema|Type $object + * @param Schema|(Type&NamedType) $object * * @return NodeList */ private function getDirectives(object $object): NodeList { $directives = []; + /** + * Excluding directiveNode, since $object is not Directive. + * + * @var SchemaDefinitionNode|SchemaTypeExtensionNode|ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode|UnionTypeDefinitionNode|UnionTypeExtensionNode|EnumTypeDefinitionNode|EnumTypeExtensionNode|InputObjectTypeDefinitionNode|InputObjectTypeExtensionNode $node + */ + // @phpstan-ignore-next-line union types are not pervasive foreach ($this->getAllNodes($object) as $node) { foreach ($node->directives as $directive) { $directives[] = $directive; diff --git a/src/Utils/AST.php b/src/Utils/AST.php index ba6cf007f..e8e7889b8 100644 --- a/src/Utils/AST.php +++ b/src/Utils/AST.php @@ -36,8 +36,10 @@ use GraphQL\Type\Definition\IDType; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputType; +use GraphQL\Type\Definition\LeafType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; +use GraphQL\Type\Definition\NullableType; use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; @@ -49,7 +51,6 @@ use function is_string; use function property_exists; use Throwable; -use Traversable; /** * Various utilities dealing with AST. @@ -148,16 +149,17 @@ public static function toArray(Node $node): array * | Mixed | Enum Value | * | null | NullValue | * - * @param Type|mixed|null $value - * @param ScalarType|EnumType|InputObjectType|ListOfType|NonNull $type + * @param mixed $value + * @param InputType&Type $type * - * @return ObjectValueNode|ListValueNode|BooleanValueNode|IntValueNode|FloatValueNode|EnumValueNode|StringValueNode|NullValueNode|null + * @return (ValueNode&Node)|null * * @api */ - public static function astFromValue($value, InputType $type) + public static function astFromValue($value, InputType $type): ?ValueNode { if ($type instanceof NonNull) { + // @phpstan-ignore-next-line wrapped type must also be input type $astValue = self::astFromValue($value, $type->getWrappedType()); if ($astValue instanceof NullValueNode) { return null; @@ -170,11 +172,13 @@ public static function astFromValue($value, InputType $type) return new NullValueNode([]); } - // Convert PHP array to GraphQL list. If the GraphQLType is a list, but + // Convert PHP iterables to GraphQL list. If the GraphQLType is a list, but // the value is not an array, convert the value using the list's item type. if ($type instanceof ListOfType) { $itemType = $type->getWrappedType(); - if (is_array($value) || ($value instanceof Traversable)) { + assert($itemType instanceof InputType, 'proven by schema validation'); + + if (is_iterable($value)) { $valuesNodes = []; foreach ($value as $item) { $itemNode = self::astFromValue($item, $itemType); @@ -240,6 +244,8 @@ public static function astFromValue($value, InputType $type) return new ObjectValueNode(['fields' => new NodeList($fieldNodes)]); } + assert($type instanceof LeafType, 'other options were exhausted'); + // Since value is an internally represented value, it must be serialized // to an externally represented value before converting into an AST. $serialized = $type->serialize($value); @@ -301,8 +307,8 @@ public static function astFromValue($value, InputType $type) * | Enum Value | Mixed | * | Null Value | null | * - * @param VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode|null $valueNode - * @param array|null $variables + * @param (ValueNode&Node)|null $valueNode + * @param array|null $variables * * @throws Exception * @@ -441,26 +447,24 @@ public static function valueFromAST(?ValueNode $valueNode, Type $type, ?array $v } } - if ($type instanceof ScalarType) { - // Scalars fulfill parsing a literal value via parseLiteral(). - // Invalid values represent a failure to parse correctly, in which case - // no value is returned. - try { - return $type->parseLiteral($valueNode, $variables); - } catch (Throwable $error) { - return $undefined; - } - } + assert($type instanceof ScalarType, 'only remaining option'); - throw new Error('Unknown type: ' . Utils::printSafe($type) . '.'); + // Scalars fulfill parsing a literal value via parseLiteral(). + // Invalid values represent a failure to parse correctly, in which case + // no value is returned. + try { + return $type->parseLiteral($valueNode, $variables); + } catch (Throwable $error) { + return $undefined; + } } /** * Returns true if the provided valueNode is a variable which is not defined * in the set of variables. * - * @param VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode $valueNode - * @param array|null $variables + * @param ValueNode&Node $valueNode + * @param array|null $variables */ private static function isMissingVariable(ValueNode $valueNode, ?array $variables): bool { @@ -557,10 +561,13 @@ public static function typeFromAST(Schema $schema, Node $inputTypeNode): ?Type if ($inputTypeNode instanceof NonNullTypeNode) { $innerType = self::typeFromAST($schema, $inputTypeNode->type); + if (null === $innerType) { + return null; + } - return null === $innerType - ? null - : new NonNull($innerType); + assert($innerType instanceof NullableType, 'proven by schema validation'); + + return new NonNull($innerType); } return $schema->getType($inputTypeNode->name->value); diff --git a/src/Utils/ASTDefinitionBuilder.php b/src/Utils/ASTDefinitionBuilder.php index f4932b9af..658cb2cfe 100644 --- a/src/Utils/ASTDefinitionBuilder.php +++ b/src/Utils/ASTDefinitionBuilder.php @@ -27,19 +27,22 @@ use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\FieldDefinition; +use GraphQL\Type\Definition\InputObjectField; use GraphQL\Type\Definition\InputObjectType; +use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\OutputType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; use function is_array; use function is_string; -use function sprintf; use Throwable; /** - * @phpstan-import-type FieldMapConfig from FieldDefinition * @phpstan-import-type UnnamedFieldDefinitionConfig from FieldDefinition + * @phpstan-import-type InputObjectFieldConfig from InputObjectField + * @phpstan-import-type UnnamedInputObjectFieldConfig from InputObjectField * @phpstan-type ResolveType callable(string, Node|null): Type * @phpstan-type TypeConfigDecorator callable(array, Node&TypeDefinitionNode, array): array */ @@ -64,7 +67,7 @@ class ASTDefinitionBuilder private array $cache; /** - * @param array $typeDefinitionsMap + * @param array $typeDefinitionsMap * @phpstan-param ResolveType $resolveType * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator */ @@ -100,15 +103,17 @@ public function buildDirective(DirectiveDefinitionNode $directiveNode): Directiv /** * @param NodeList $values * - * @return array> + * @return array */ private function makeInputValues(NodeList $values): array { + /** @var array $map */ $map = []; foreach ($values as $value) { // Note: While this could make assertions to get the correctly typed // value, that would throw immediately while type system validation // with validateSchema() will produce more actionable results. + /** @var Type&InputType $type */ $type = $this->buildWrappedType($value->type); $config = [ @@ -117,7 +122,8 @@ private function makeInputValues(NodeList $values): array 'description' => $value->description->value ?? null, 'astNode' => $value, ]; - if (isset($value->defaultValue)) { + + if (null !== $value->defaultValue) { $config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type); } @@ -137,6 +143,7 @@ private function buildWrappedType(TypeNode $typeNode): Type } if ($typeNode instanceof NonNullTypeNode) { + // @phpstan-ignore-next-line contained type is NullableType return Type::nonNull($this->buildWrappedType($typeNode->type)); } @@ -144,7 +151,7 @@ private function buildWrappedType(TypeNode $typeNode): Type } /** - * @param string|(Node &NamedTypeNode)|(Node&TypeDefinitionNode) $ref + * @param string|(Node&NamedTypeNode)|(Node&TypeDefinitionNode) $ref */ public function buildType($ref): Type { @@ -156,7 +163,7 @@ public function buildType($ref): Type } /** - * @param (Node &NamedTypeNode)|(Node&TypeDefinitionNode)|null $typeNode + * @param (Node&NamedTypeNode)|(Node&TypeDefinitionNode)|null $typeNode * * @throws Error */ @@ -204,7 +211,7 @@ private function internalBuildType(string $typeName, ?Node $typeNode = null): Ty } /** - * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|EnumTypeDefinitionNode|ScalarTypeDefinitionNode|InputObjectTypeDefinitionNode|UnionTypeDefinitionNode $def + * @param TypeDefinitionNode&Node $def * * @throws Error * @@ -229,6 +236,8 @@ private function makeSchemaDef(Node $def): Type return $this->makeScalarDef($def); default: + assert($def instanceof InputObjectTypeDefinitionNode, 'all implementations are known'); + return $this->makeInputObjectDef($def); } } @@ -264,11 +273,14 @@ private function makeFieldDefMap(Node $def): array */ public function buildField(FieldDefinitionNode $field): array { + // Note: While this could make assertions to get the correctly typed + // value, that would throw immediately while type system validation + // with validateSchema() will produce more actionable results. + /** @var OutputType&Type $type */ + $type = $this->buildWrappedType($field->type); + return [ - // Note: While this could make assertions to get the correctly typed - // value, that would throw immediately while type system validation - // with validateSchema() will produce more actionable results. - 'type' => $this->buildWrappedType($field->type), + 'type' => $type, 'description' => $field->description->value ?? null, 'args' => $this->makeInputValues($field->arguments), 'deprecationReason' => $this->getDeprecationReason($field), @@ -356,6 +368,7 @@ private function makeUnionDef(UnionTypeDefinitionNode $def): UnionType $types[] = $this->buildType($type); } + /** @var array $types */ return $types; }, 'astNode' => $def, @@ -393,34 +406,40 @@ private function makeSchemaDefFromConfig(Node $def, array $config): Type { switch (true) { case $def instanceof ObjectTypeDefinitionNode: + // @phpstan-ignore-next-line assume the config matches return new ObjectType($config); case $def instanceof InterfaceTypeDefinitionNode: + // @phpstan-ignore-next-line assume the config matches return new InterfaceType($config); case $def instanceof EnumTypeDefinitionNode: + // @phpstan-ignore-next-line assume the config matches return new EnumType($config); case $def instanceof UnionTypeDefinitionNode: + // @phpstan-ignore-next-line assume the config matches return new UnionType($config); case $def instanceof ScalarTypeDefinitionNode: return new CustomScalarType($config); case $def instanceof InputObjectTypeDefinitionNode: + // @phpstan-ignore-next-line assume the config matches return new InputObjectType($config); default: - throw new Error(sprintf('Type kind of %s not supported.', $def->kind)); + throw new Error("Type kind of {$def->kind} not supported."); } } /** - * @return array + * @return InputObjectFieldConfig */ public function buildInputField(InputValueDefinitionNode $value): array { $type = $this->buildWrappedType($value->type); + assert($type instanceof InputType, 'proven by schema validation'); $config = [ 'name' => $value->name->value, @@ -430,7 +449,7 @@ public function buildInputField(InputValueDefinitionNode $value): array ]; if (null !== $value->defaultValue) { - $config['defaultValue'] = $value->defaultValue; + $config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type); } return $config; diff --git a/src/Utils/BreakingChangesFinder.php b/src/Utils/BreakingChangesFinder.php index eaaea71b4..0f94b98ab 100644 --- a/src/Utils/BreakingChangesFinder.php +++ b/src/Utils/BreakingChangesFinder.php @@ -151,7 +151,7 @@ public static function findTypesThatChangedKind( } /** - * @param Type &NamedType $type + * @param Type&NamedType $type */ private static function typeKindName(NamedType $type): string { diff --git a/src/Utils/BuildClientSchema.php b/src/Utils/BuildClientSchema.php index f0312b91c..86ff097d2 100644 --- a/src/Utils/BuildClientSchema.php +++ b/src/Utils/BuildClientSchema.php @@ -13,13 +13,13 @@ use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\FieldDefinition; +use GraphQL\Type\Definition\InputObjectField; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NamedType; use GraphQL\Type\Definition\NonNull; -use GraphQL\Type\Definition\NullableType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\OutputType; use GraphQL\Type\Definition\ScalarType; @@ -33,6 +33,7 @@ /** * @phpstan-import-type UnnamedFieldDefinitionConfig from FieldDefinition + * @phpstan-import-type UnnamedInputObjectFieldConfig from InputObjectField * @phpstan-type Options array{ * assumeValid?: bool, * } @@ -168,10 +169,8 @@ private function getType(array $typeRef): Type throw new InvariantViolation('Decorated type deeper than introspection query.'); } - /** @var NullableType $nullableType */ - $nullableType = $this->getType($typeRef['ofType']); - - return new NonNull($nullableType); + // @phpstan-ignore-next-line if the type is not a nullable type, schema validation will catch it + return new NonNull($this->getType($typeRef['ofType'])); } } @@ -368,7 +367,7 @@ private function buildInterfaceDef(array $interface): InterfaceType } /** - * @param array> $union + * @param array $union */ private function buildUnionDef(array $union): UnionType { @@ -387,7 +386,7 @@ private function buildUnionDef(array $union): UnionType } /** - * @param array> $enum + * @param array $enum */ private function buildEnumDef(array $enum): EnumType { @@ -439,6 +438,7 @@ private function buildFieldDefMap(array $typeIntrospection): array throw new InvariantViolation('Introspection result missing fields: ' . json_encode($typeIntrospection) . '.'); } + /** @var array $map */ $map = []; foreach ($typeIntrospection['fields'] as $field) { if (! array_key_exists('args', $field)) { @@ -453,28 +453,31 @@ private function buildFieldDefMap(array $typeIntrospection): array ]; } + // @phpstan-ignore-next-line unless the returned name was numeric, this works return $map; } /** * @param array> $inputValueIntrospections * - * @return array> + * @return array */ private function buildInputValueDefMap(array $inputValueIntrospections): array { + /** @var array $map */ $map = []; foreach ($inputValueIntrospections as $value) { $map[$value['name']] = $this->buildInputValue($value); } + // @phpstan-ignore-next-line unless the returned name was numeric, this works return $map; } /** * @param array $inputValueIntrospection * - * @return array + * @return UnnamedInputObjectFieldConfig */ public function buildInputValue(array $inputValueIntrospection): array { diff --git a/src/Utils/BuildSchema.php b/src/Utils/BuildSchema.php index c6bf4ac0e..cc2f016cd 100644 --- a/src/Utils/BuildSchema.php +++ b/src/Utils/BuildSchema.php @@ -132,8 +132,10 @@ public function buildSchema(): Schema $schemaDef = null; $this->nodeMap = []; + /** @var array $directiveDefs */ $directiveDefs = []; + foreach ($this->ast->definitions as $definition) { switch (true) { case $definition instanceof SchemaDefinitionNode: diff --git a/src/Utils/SchemaExtender.php b/src/Utils/SchemaExtender.php index 1afd7cf54..bf0138d37 100644 --- a/src/Utils/SchemaExtender.php +++ b/src/Utils/SchemaExtender.php @@ -16,6 +16,7 @@ use GraphQL\Language\AST\InterfaceTypeExtensionNode; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\ObjectTypeExtensionNode; +use GraphQL\Language\AST\ScalarTypeExtensionNode; use GraphQL\Language\AST\SchemaDefinitionNode; use GraphQL\Language\AST\SchemaTypeExtensionNode; use GraphQL\Language\AST\TypeDefinitionNode; @@ -26,7 +27,9 @@ use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\ImplementingType; +use GraphQL\Type\Definition\InputObjectField; use GraphQL\Type\Definition\InputObjectType; +use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NamedType; @@ -41,6 +44,8 @@ /** * @phpstan-import-type TypeConfigDecorator from ASTDefinitionBuilder + * @phpstan-import-type UnnamedArgumentConfig from Argument + * @phpstan-import-type UnnamedInputObjectFieldConfig from InputObjectField */ class SchemaExtender { @@ -53,11 +58,11 @@ class SchemaExtender protected static ASTDefinitionBuilder $astBuilder; /** - * @param Type &NamedType $type + * @param Type&NamedType $type * * @return array|null */ - protected static function getExtensionASTNodes(NamedType $type): ?array + protected static function extensionASTNodes(NamedType $type): ?array { return array_merge( $type->extensionASTNodes ?? [], @@ -66,7 +71,7 @@ protected static function getExtensionASTNodes(NamedType $type): ?array } /** - * @param Type &NamedType $type + * @param Type&NamedType $type * * @throws Error */ @@ -123,6 +128,9 @@ protected static function assertTypeMatchesExtension(NamedType $type, Node $node protected static function extendScalarType(ScalarType $type): CustomScalarType { + /** @var array $extensionASTNodes */ + $extensionASTNodes = static::extensionASTNodes($type); + return new CustomScalarType([ 'name' => $type->name, 'description' => $type->description, @@ -130,72 +138,83 @@ protected static function extendScalarType(ScalarType $type): CustomScalarType 'parseValue' => [$type, 'parseValue'], 'parseLiteral' => [$type, 'parseLiteral'], 'astNode' => $type->astNode, - 'extensionASTNodes' => static::getExtensionASTNodes($type), + 'extensionASTNodes' => $extensionASTNodes, ]); } protected static function extendUnionType(UnionType $type): UnionType { + /** @var array $extensionASTNodes */ + $extensionASTNodes = static::extensionASTNodes($type); + return new UnionType([ 'name' => $type->name, 'description' => $type->description, 'types' => static fn (): array => static::extendUnionPossibleTypes($type), 'resolveType' => [$type, 'resolveType'], 'astNode' => $type->astNode, - 'extensionASTNodes' => static::getExtensionASTNodes($type), + 'extensionASTNodes' => $extensionASTNodes, ]); } protected static function extendEnumType(EnumType $type): EnumType { + /** @var array $extensionASTNodes */ + $extensionASTNodes = static::extensionASTNodes($type); + return new EnumType([ 'name' => $type->name, 'description' => $type->description, 'values' => static::extendEnumValueMap($type), 'astNode' => $type->astNode, - 'extensionASTNodes' => static::getExtensionASTNodes($type), + 'extensionASTNodes' => $extensionASTNodes, ]); } protected static function extendInputObjectType(InputObjectType $type): InputObjectType { + /** @var array $extensionASTNodes */ + $extensionASTNodes = static::extensionASTNodes($type); + return new InputObjectType([ 'name' => $type->name, 'description' => $type->description, 'fields' => static fn (): array => static::extendInputFieldMap($type), 'astNode' => $type->astNode, - 'extensionASTNodes' => static::getExtensionASTNodes($type), + 'extensionASTNodes' => $extensionASTNodes, ]); } /** - * @return array> + * @return array */ protected static function extendInputFieldMap(InputObjectType $type): array { + /** @var array $newFieldMap */ $newFieldMap = []; + $oldFieldMap = $type->getFields(); foreach ($oldFieldMap as $fieldName => $field) { - $newFieldMap[$fieldName] = [ + $extendedType = static::extendType($field->getType()); + assert($extendedType instanceof InputType, 'proven by schema validation'); + + $newFieldConfig = [ 'description' => $field->description, - 'type' => static::extendType($field->getType()), + 'type' => $extendedType, 'astNode' => $field->astNode, ]; - if (! $field->defaultValueExists()) { - continue; + if ($field->defaultValueExists()) { + $newFieldConfig['defaultValue'] = $field->defaultValue; } - $newFieldMap[$fieldName]['defaultValue'] = $field->defaultValue; + $newFieldMap[$fieldName] = $newFieldConfig; } if (isset(static::$typeExtensionsMap[$type->name])) { - /** - * Proven by @see assertTypeMatchesExtension(). - * - * @var InputObjectTypeExtensionNode $extension - */ foreach (static::$typeExtensionsMap[$type->name] as $extension) { + assert($extension instanceof InputObjectTypeExtensionNode, 'proven by assertTypeMatchesExtension()'); + foreach ($extension->fields as $field) { $fieldName = $field->name->value; if (isset($oldFieldMap[$fieldName])) { @@ -228,12 +247,9 @@ protected static function extendEnumValueMap(EnumType $type): array } if (isset(static::$typeExtensionsMap[$type->name])) { - /** - * Proven by @see assertTypeMatchesExtension(). - * - * @var EnumTypeExtensionNode $extension - */ foreach (static::$typeExtensionsMap[$type->name] as $extension) { + assert($extension instanceof EnumTypeExtensionNode, 'proven by assertTypeMatchesExtension()'); + foreach ($extension->values as $value) { $newValueMap[$value->name->value] = static::$astBuilder->buildEnumValue($value); } @@ -244,7 +260,7 @@ protected static function extendEnumValueMap(EnumType $type): array } /** - * @return array Should be ObjectType, will be caught in schema validation + * @return array */ protected static function extendUnionPossibleTypes(UnionType $type): array { @@ -254,25 +270,23 @@ protected static function extendUnionPossibleTypes(UnionType $type): array ); if (isset(static::$typeExtensionsMap[$type->name])) { - /** - * Proven by @see assertTypeMatchesExtension(). - * - * @var UnionTypeExtensionNode $extension - */ foreach (static::$typeExtensionsMap[$type->name] as $extension) { + assert($extension instanceof UnionTypeExtensionNode, 'proven by assertTypeMatchesExtension()'); + foreach ($extension->types as $namedType) { $possibleTypes[] = static::$astBuilder->buildType($namedType); } } } + // @phpstan-ignore-next-line proven by schema validation return $possibleTypes; } /** * @param ObjectType|InterfaceType $type * - * @return array Should be InterfaceType, will be caught in schema validation + * @return array */ protected static function extendImplementedInterfaces(ImplementingType $type): array { @@ -282,49 +296,62 @@ protected static function extendImplementedInterfaces(ImplementingType $type): a ); if (isset(static::$typeExtensionsMap[$type->name])) { - /** - * Proven by @see assertTypeMatchesExtension(). - * - * @var ObjectTypeExtensionNode|InterfaceTypeExtensionNode $extension - */ foreach (static::$typeExtensionsMap[$type->name] as $extension) { + assert( + $extension instanceof ObjectTypeExtensionNode || $extension instanceof InterfaceTypeExtensionNode, + 'proven by assertTypeMatchesExtension()' + ); + foreach ($extension->interfaces as $namedType) { - /** @var InterfaceType $interface we know this, but PHP templates cannot express it */ $interface = static::$astBuilder->buildType($namedType); + assert($interface instanceof InterfaceType, 'we know this, but PHP templates cannot express it'); + $interfaces[] = $interface; } } } + // @phpstan-ignore-next-line will be caught in schema validation return $interfaces; } + /** + * @template T of Type + * + * @param T $typeDef + * + * @return T + */ protected static function extendType(Type $typeDef): Type { if ($typeDef instanceof ListOfType) { + // @phpstan-ignore-next-line PHPStan does not understand this is the same generic type as the input return Type::listOf(static::extendType($typeDef->getWrappedType())); } if ($typeDef instanceof NonNull) { + // @phpstan-ignore-next-line PHPStan does not understand this is the same generic type as the input return Type::nonNull(static::extendType($typeDef->getWrappedType())); } - /** @var NamedType&Type $typeDef */ - + // @phpstan-ignore-next-line PHPStan does not understand this is the same generic type as the input return static::extendNamedType($typeDef); } /** * @param array $args * - * @return array> + * @return array */ protected static function extendArgs(array $args): array { $extended = []; foreach ($args as $arg) { + $extendedType = static::extendType($arg->getType()); + assert($extendedType instanceof InputType, 'proven by schema validation'); + $def = [ - 'type' => static::extendType($arg->getType()), + 'type' => $extendedType, 'description' => $arg->description, 'astNode' => $arg->astNode, ]; @@ -366,12 +393,12 @@ protected static function extendFieldMap(Type $type): array } if (isset(static::$typeExtensionsMap[$type->name])) { - /** - * Proven by @see assertTypeMatchesExtension(). - * - * @var ObjectTypeExtensionNode|InputObjectTypeExtensionNode $extension - */ foreach (static::$typeExtensionsMap[$type->name] as $extension) { + assert( + $extension instanceof ObjectTypeExtensionNode || $extension instanceof InterfaceTypeExtensionNode, + 'proven by assertTypeMatchesExtension()' + ); + foreach ($extension->fields as $field) { $fieldName = $field->name->value; if (isset($oldFieldMap[$fieldName])) { @@ -388,6 +415,9 @@ protected static function extendFieldMap(Type $type): array protected static function extendObjectType(ObjectType $type): ObjectType { + /** @var array $extensionASTNodes */ + $extensionASTNodes = static::extensionASTNodes($type); + return new ObjectType([ 'name' => $type->name, 'description' => $type->description, @@ -396,12 +426,15 @@ protected static function extendObjectType(ObjectType $type): ObjectType 'isTypeOf' => [$type, 'isTypeOf'], 'resolveField' => $type->resolveFieldFn ?? null, 'astNode' => $type->astNode, - 'extensionASTNodes' => static::getExtensionASTNodes($type), + 'extensionASTNodes' => $extensionASTNodes, ]); } protected static function extendInterfaceType(InterfaceType $type): InterfaceType { + /** @var array $extensionASTNodes */ + $extensionASTNodes = static::extensionASTNodes($type); + return new InterfaceType([ 'name' => $type->name, 'description' => $type->description, @@ -409,7 +442,7 @@ protected static function extendInterfaceType(InterfaceType $type): InterfaceTyp 'fields' => static fn (): array => static::extendFieldMap($type), 'resolveType' => [$type, 'resolveType'], 'astNode' => $type->astNode, - 'extensionASTNodes' => static::getExtensionASTNodes($type), + 'extensionASTNodes' => $extensionASTNodes, ]); } @@ -426,11 +459,11 @@ protected static function isSpecifiedScalarType(Type $type): bool } /** - * @param T &NamedType $type + * @template T of Type * - * @return T&NamedType + * @param T&NamedType $type * - * @template T of Type + * @return T&NamedType */ protected static function extendNamedType(Type $type): Type { @@ -460,11 +493,11 @@ protected static function extendNamedType(Type $type): Type } /** - * @param (T &NamedType)|null $type + * @template T of Type * - * @return (T&NamedType)|null + * @param (T&NamedType)|null $type * - * @template T of Type + * @return (T&NamedType)|null */ protected static function extendMaybeNamedType(?Type $type = null): ?Type { @@ -527,10 +560,15 @@ public static function extend( /** @var array $typeDefinitionMap */ $typeDefinitionMap = []; + static::$typeExtensionsMap = []; + + /** @var array $directiveDefinitions */ $directiveDefinitions = []; + /** @var SchemaDefinitionNode|null $schemaDef */ $schemaDef = null; + /** @var array $schemaExtensions */ $schemaExtensions = []; diff --git a/src/Utils/SchemaPrinter.php b/src/Utils/SchemaPrinter.php index cf572df20..46147b727 100644 --- a/src/Utils/SchemaPrinter.php +++ b/src/Utils/SchemaPrinter.php @@ -205,7 +205,7 @@ protected static function printDirective(Directive $directive, array $options): /** * @param array $options - * @param Type|Directive|EnumValueDefinition|Argument|FieldDefinition|InputObjectField $def + * @param (Type&NamedType)|Directive|EnumValueDefinition|Argument|FieldDefinition|InputObjectField $def */ protected static function printDescription(array $options, $def, string $indentation = '', bool $firstInBlock = true): string { @@ -252,12 +252,18 @@ protected static function printArgs(array $options, array $args, string $indenta if ( Utils::every( $args, - static function ($arg): bool { - return 0 === strlen($arg->description ?? ''); - } + static fn (Argument $arg): bool => 0 === strlen($arg->description ?? '') ) ) { - return '(' . implode(', ', array_map('static::printInputValue', $args)) . ')'; + return '(' + . implode( + ', ', + array_map( + [static::class, 'printInputValue'], + $args + ) + ) + . ')'; } return sprintf( diff --git a/src/Utils/TypeComparators.php b/src/Utils/TypeComparators.php index 9f37f55ac..3a3544662 100644 --- a/src/Utils/TypeComparators.php +++ b/src/Utils/TypeComparators.php @@ -4,16 +4,12 @@ namespace GraphQL\Utils; -use GraphQL\Type\Definition\AbstractType; use GraphQL\Type\Definition\ImplementingType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; -/** - * @phpstan-import-type AbstractTypeAlias from AbstractType - */ class TypeComparators { /** @@ -82,7 +78,6 @@ public static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type if (Type::isAbstractType($superType)) { // If superType type is an abstract type, maybeSubType type may be a currently // possible object or interface type. - /** @phpstan-var AbstractTypeAlias $superType proven by Type::isAbstractType() */ return $maybeSubType instanceof ImplementingType && $schema->isSubType($superType, $maybeSubType); diff --git a/src/Utils/TypeInfo.php b/src/Utils/TypeInfo.php index 47318828d..7a9e404d3 100644 --- a/src/Utils/TypeInfo.php +++ b/src/Utils/TypeInfo.php @@ -43,26 +43,23 @@ use GraphQL\Type\Introspection; use GraphQL\Type\Schema; -/** - * @phpstan-import-type InputTypeAlias from InputType - */ class TypeInfo { private Schema $schema; - /** @var array<(OutputType&Type)|null> */ + /** @var array */ private array $typeStack = []; - /** @var array<(CompositeType&Type)|null> */ + /** @var array */ private array $parentTypeStack = []; - /** @var array<(InputType&Type)|null> */ + /** @var array */ private array $inputTypeStack = []; - /** @var array */ + /** @var array */ private array $fieldDefStack = []; - /** @var array */ + /** @var array */ private array $defaultValueStack = []; private ?Directive $directive = null; @@ -90,17 +87,18 @@ public function __construct(Schema $schema) * ... * ] * - * @param array $typeMap + * @param array $typeMap */ public static function extractTypes(Type $type, array &$typeMap): void { - /** @var (Type&WrappingType)|(Type&NamedType) $type */ if ($type instanceof WrappingType) { self::extractTypes($type->getInnermostType(), $typeMap); return; } + assert($type instanceof NamedType, 'only other option'); + $name = $type->name; if (isset($typeMap[$name])) { @@ -151,7 +149,7 @@ public static function extractTypes(Type $type, array &$typeMap): void } /** - * @param array $typeMap + * @param array $typeMap */ public static function extractTypesFromDirectives(Directive $directive, array &$typeMap): void { @@ -221,7 +219,8 @@ public function enter(Node $node): void $type = $schema->getQueryType(); } elseif ('mutation' === $node->operation) { $type = $schema->getMutationType(); - } elseif ('subscription' === $node->operation) { + } else { + // Only other option $type = $schema->getSubscriptionType(); } @@ -366,7 +365,7 @@ private static function getFieldDefinition(Schema $schema, Type $parentType, Fie * * @throws InvariantViolation */ - public static function typeFromAST(Schema $schema, $inputTypeNode): ?Type + public static function typeFromAST(Schema $schema, Node $inputTypeNode): ?Type { return AST::typeFromAST($schema, $inputTypeNode); } @@ -390,7 +389,7 @@ public function getDefaultValue() } /** - * @phpstan-return InputTypeAlias|null + * @return (InputType&Type)|null */ public function getInputType(): ?InputType { diff --git a/src/Utils/Utils.php b/src/Utils/Utils.php index c460041f7..8eb8940e5 100644 --- a/src/Utils/Utils.php +++ b/src/Utils/Utils.php @@ -69,7 +69,7 @@ public static function assign(object $obj, array $vars): object if (! property_exists($obj, $key)) { $cls = get_class($obj); Warning::warn( - sprintf("Trying to set non-existing property '%s' on class '%s'", $key, $cls), + "Trying to set non-existing property '{$key}' on class '{$cls}'", Warning::WARNING_ASSIGN ); } @@ -81,15 +81,15 @@ public static function assign(object $obj, array $vars): object } /** + * @template TKey of array-key + * @template TValue + * * @param iterable $iterable * @phpstan-param iterable $iterable * @phpstan-param callable(TValue, TKey): bool $predicate * * @return mixed * @phpstan-return TValue|null - * - * @template TKey of array-key - * @template TValue */ public static function find(iterable $iterable, callable $predicate) { @@ -159,11 +159,11 @@ public static function invariant($test, $message = ''): void public static function printSafeJson($var): string { if ($var instanceof stdClass) { - return json_encode($var); + return json_encode($var, JSON_THROW_ON_ERROR); } if (is_array($var)) { - return json_encode($var); + return json_encode($var, JSON_THROW_ON_ERROR); } if ('' === $var) { @@ -211,7 +211,7 @@ public static function printSafe($var): string } if (is_array($var)) { - return json_encode($var); + return json_encode($var, JSON_THROW_ON_ERROR); } if ('' === $var) { @@ -266,37 +266,30 @@ public static function ord(string $char, string $encoding = 'UTF-8'): int $char = mb_convert_encoding($char, 'UCS-4BE', $encoding); } + // @phpstan-ignore-next-line format string is statically known to be correct return unpack('N', $char)[1]; } /** * Returns UTF-8 char code at given $positing of the $string. - * - * @param string $string - * @param int $position - * - * @return mixed */ - public static function charCodeAt($string, $position) + public static function charCodeAt(string $string, int $position): int { $char = mb_substr($string, $position, 1, 'UTF-8'); return self::ord($char); } - /** - * @param int|null $code - */ - public static function printCharCode($code): string + public static function printCharCode(?int $code): string { if (null === $code) { return ''; } return $code < 0x007F - // Trust JSON for ASCII. - ? json_encode(self::chr($code)) - // Otherwise print the escaped form. + // Trust JSON for ASCII + ? json_encode(self::chr($code), JSON_THROW_ON_ERROR) + // Otherwise, print the escaped form : '"\\u' . dechex($code) . '"'; } @@ -325,7 +318,7 @@ public static function isValidNameError(string $name, ?Node $node = null): ?Erro ); } - if (! preg_match('/^[_a-zA-Z][_a-zA-Z0-9]*$/', $name)) { + if (1 !== preg_match('/^[_a-zA-Z][_a-zA-Z0-9]*$/', $name)) { return new Error( 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "' . $name . '" does not.', $node diff --git a/src/Utils/Value.php b/src/Utils/Value.php index d3d33a137..8285bfb28 100644 --- a/src/Utils/Value.php +++ b/src/Utils/Value.php @@ -34,15 +34,15 @@ * @phpstan-type CoercedErrors array{errors: array, value: null} * * The key prev should actually also be typed as Path, but PHPStan does not support recursive types. - * @phpstan-type Path array{prev: array, key: string|int} + * @phpstan-type Path array{prev: array|null, key: string|int} */ class Value { /** * Given a type and any value, return a runtime value coerced to match the type. * - * @param mixed $value - * @param ScalarType|EnumType|InputObjectType|ListOfType|NonNull $type + * @param mixed $value + * @param InputType&Type $type * @phpstan-param Path|null $path * * @phpstan-return CoercedValue|CoercedErrors @@ -60,6 +60,7 @@ public static function coerceValue($value, InputType $type, ?VariableDefinitionN ]); } + // @phpstan-ignore-next-line wrapped type is known to be input type after schema validation return self::coerceValue($value, $type->getWrappedType(), $blameNode, $path); } @@ -117,6 +118,8 @@ public static function coerceValue($value, InputType $type, ?VariableDefinitionN if ($type instanceof ListOfType) { $itemType = $type->getWrappedType(); + assert($itemType instanceof InputType, 'known through schema validation'); + if (is_array($value) || $value instanceof Traversable) { $errors = []; $coercedValue = []; @@ -148,6 +151,8 @@ public static function coerceValue($value, InputType $type, ?VariableDefinitionN : self::ofValue([$coercedItem['value']]); } + assert($type instanceof InputObjectType, 'we handled all other cases at this point'); + if ($value instanceof stdClass) { // Cast objects to associative array before checking the fields. // Note that the coerced value will be an array. diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index 34dc81dc0..d184f8764 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -114,7 +114,7 @@ public static function validate( /** * Returns all global validation rules. * - * @return array, ValidationRule> + * @return array * * @api */ @@ -219,22 +219,16 @@ public static function visitUsingRules(Schema $schema, TypeInfo $typeInfo, Docum } /** - * Returns global validation rule by name. Standard rules are named by class name, so - * example usage for such rules:. - * - * $rule = DocumentValidator::getRule(GraphQL\Validator\Rules\QueryComplexity::class); - * - * @param class-string $name + * Returns global validation rule by name. * - * @return T|null + * Standard rules are named by class name, so example usage for such rules: * - * @template T of ValidationRule + * @example DocumentValidator::getRule(GraphQL\Validator\Rules\QueryComplexity::class); * * @api */ public static function getRule(string $name): ?ValidationRule { - // @phpstan-ignore-next-line class-strings are always mapped to a matching class instance return static::allRules()[$name] ?? null; } diff --git a/src/Validator/Rules/ExecutableDefinitions.php b/src/Validator/Rules/ExecutableDefinitions.php index dbd30f4da..3a1595df3 100644 --- a/src/Validator/Rules/ExecutableDefinitions.php +++ b/src/Validator/Rules/ExecutableDefinitions.php @@ -25,16 +25,15 @@ public function getVisitor(ValidationContext $context): array { return [ NodeKind::DOCUMENT => static function (DocumentNode $node) use ($context): VisitorOperation { - /** @var ExecutableDefinitionNode|TypeSystemDefinitionNode $definition */ foreach ($node->definitions as $definition) { - if ($definition instanceof ExecutableDefinitionNode) { - continue; - } + if (! $definition instanceof ExecutableDefinitionNode) { + assert($definition instanceof TypeSystemDefinitionNode, 'only other option'); - $context->reportError(new Error( - static::nonExecutableDefinitionMessage($definition->name->value), - [$definition->name] - )); + $context->reportError(new Error( + static::nonExecutableDefinitionMessage($definition->name->value), + [$definition->name] + )); + } } return Visitor::skipNode(); diff --git a/src/Validator/Rules/FieldsOnCorrectType.php b/src/Validator/Rules/FieldsOnCorrectType.php index aac429d0b..0dabc37bd 100644 --- a/src/Validator/Rules/FieldsOnCorrectType.php +++ b/src/Validator/Rules/FieldsOnCorrectType.php @@ -10,7 +10,6 @@ use GraphQL\Error\Error; use GraphQL\Language\AST\FieldNode; use GraphQL\Language\AST\NodeKind; -use GraphQL\Type\Definition\AbstractType; use GraphQL\Type\Definition\HasFieldsType; use GraphQL\Type\Definition\NamedType; use GraphQL\Type\Definition\Type; @@ -18,9 +17,6 @@ use GraphQL\Utils\Utils; use GraphQL\Validator\ValidationContext; -/** - * @phpstan-import-type AbstractTypeAlias from AbstractType - */ class FieldsOnCorrectType extends ValidationRule { public function getVisitor(ValidationContext $context): array @@ -72,7 +68,6 @@ public function getVisitor(ValidationContext $context): array protected function getSuggestedTypeNames(Schema $schema, Type $type, string $fieldName): array { if (Type::isAbstractType($type)) { - /** @phpstan-var AbstractTypeAlias $type proven by Type::isAbstractType() */ $suggestedObjectTypes = []; $interfaceUsageCount = []; diff --git a/src/Validator/Rules/KnownArgumentNamesOnDirectives.php b/src/Validator/Rules/KnownArgumentNamesOnDirectives.php index 195d4a058..5f28f338e 100644 --- a/src/Validator/Rules/KnownArgumentNamesOnDirectives.php +++ b/src/Validator/Rules/KnownArgumentNamesOnDirectives.php @@ -74,31 +74,24 @@ public function getASTVisitor(ASTValidationContext $context): array $astDefinitions = $context->getDocument()->definitions; foreach ($astDefinitions as $def) { - if (! ($def instanceof DirectiveDefinitionNode)) { - continue; - } - - $name = $def->name->value; - if (null !== $def->arguments) { + if ($def instanceof DirectiveDefinitionNode) { $argNames = []; foreach ($def->arguments as $arg) { $argNames[] = $arg->name->value; } - $directiveArgs[$name] = $argNames; - } else { - $directiveArgs[$name] = []; + $directiveArgs[$def->name->value] = $argNames; } } return [ NodeKind::DIRECTIVE => static function (DirectiveNode $directiveNode) use ($directiveArgs, $context): VisitorOperation { $directiveName = $directiveNode->name->value; - $knownArgs = $directiveArgs[$directiveName] ?? null; - if (null === $directiveNode->arguments || null === $knownArgs) { + if (! isset($directiveArgs[$directiveName])) { return Visitor::skipNode(); } + $knownArgs = $directiveArgs[$directiveName]; foreach ($directiveNode->arguments as $argNode) { $argName = $argNode->name->value; diff --git a/src/Validator/Rules/NoUnusedVariables.php b/src/Validator/Rules/NoUnusedVariables.php index 33cfa740e..823a0626d 100644 --- a/src/Validator/Rules/NoUnusedVariables.php +++ b/src/Validator/Rules/NoUnusedVariables.php @@ -12,7 +12,7 @@ class NoUnusedVariables extends ValidationRule { - /** @var VariableDefinitionNode[] */ + /** @var array */ protected array $variableDefs; public function getVisitor(ValidationContext $context): array diff --git a/src/Validator/Rules/OverlappingFieldsCanBeMerged.php b/src/Validator/Rules/OverlappingFieldsCanBeMerged.php index 9fce2de3a..878cb72ad 100644 --- a/src/Validator/Rules/OverlappingFieldsCanBeMerged.php +++ b/src/Validator/Rules/OverlappingFieldsCanBeMerged.php @@ -7,7 +7,6 @@ use function array_keys; use function array_map; use function array_merge; -use function array_reduce; use function count; use GraphQL\Error\Error; use GraphQL\Language\AST\ArgumentNode; @@ -34,8 +33,11 @@ use SplObjectStorage; /** - * @phpstan-type ReasonOrReasons string|array}> - * @phpstan-type Conflict array{array{string, ReasonOrReasons}, array{FieldNode}, array{FieldNode}} + * ReasonOrReasons is recursive, but PHPStan does not support that. + * + * @phpstan-type ReasonOrReasons string|array}> + * + * @phpstan-type Conflict array{array{string, ReasonOrReasons}, array, array} * @phpstan-type FieldInfo array{Type, FieldNode, FieldDefinition|null} * @phpstan-type FieldMap array> */ @@ -235,7 +237,7 @@ protected function getFieldsAndFragmentNames( * as well as a list of nested fragment names referenced via fragment spreads. * * @param array $fragmentNames - * @phpstan-param FieldMap $astAndDefs + * @phpstan-param FieldMap $astAndDefs */ protected function internalCollectFieldsAndFragmentNames( ValidationContext $context, @@ -336,7 +338,7 @@ protected function collectConflictsWithin( * @param array{Type, FieldNode, FieldDefinition|null} $field1 * @param array{Type, FieldNode, FieldDefinition|null} $field2 * - * @return array{array{string, string}, array{FieldNode}, array{FieldNode}}|null + * @phpstan-return Conflict|null */ protected function findConflict( ValidationContext $context, @@ -813,8 +815,7 @@ protected function collectConflictsBetweenFragments( } /** - * Given a series of Conflicts which occurred between two sub-fields, generate - * a single Conflict. + * Merge Conflicts between two sub-fields into a single Conflict. * * @phpstan-param array $conflicts * @@ -830,28 +831,32 @@ protected function subfieldConflicts( return null; } + $reasons = []; + foreach ($conflicts as $conflict) { + $reasons[] = $conflict[0]; + } + + $fields1 = [$ast1]; + foreach ($conflicts as $conflict) { + foreach ($conflict[1] as $field) { + $fields1[] = $field; + } + } + + $fields2 = [$ast2]; + foreach ($conflicts as $conflict) { + foreach ($conflict[2] as $field) { + $fields2[] = $field; + } + } + return [ [ $responseName, - array_map( - static fn (array $conflict) => $conflict[0], - $conflicts - ), + $reasons, ], - array_reduce( - $conflicts, - static function ($allFields, $conflict): array { - return array_merge($allFields, $conflict[1]); - }, - [$ast1] - ), - array_reduce( - $conflicts, - static function ($allFields, $conflict): array { - return array_merge($allFields, $conflict[2]); - }, - [$ast2] - ), + $fields1, + $fields2, ]; } diff --git a/src/Validator/Rules/PossibleFragmentSpreads.php b/src/Validator/Rules/PossibleFragmentSpreads.php index f483ee6e5..678e07dd5 100644 --- a/src/Validator/Rules/PossibleFragmentSpreads.php +++ b/src/Validator/Rules/PossibleFragmentSpreads.php @@ -62,8 +62,8 @@ public function getVisitor(ValidationContext $context): array } /** - * @param CompositeType &Type $fragType - * @param CompositeType &Type $parentType + * @param CompositeType&Type $fragType + * @param CompositeType&Type $parentType */ protected function doTypesOverlap(Schema $schema, CompositeType $fragType, CompositeType $parentType): bool { diff --git a/src/Validator/Rules/QueryComplexity.php b/src/Validator/Rules/QueryComplexity.php index 3d544703b..6d2bf8790 100644 --- a/src/Validator/Rules/QueryComplexity.php +++ b/src/Validator/Rules/QueryComplexity.php @@ -168,34 +168,34 @@ protected function directiveExcludesField(FieldNode $node): bool $this->variableDefs, $this->getRawVariableValues() ); - if (count($errors ?? []) > 0) { + if (null !== $errors && count($errors) > 0) { throw new Error(implode( "\n\n", array_map( - static fn ($error) => $error->getMessage(), + static fn (Error $error): string => $error->getMessage(), $errors ) )); } if (Directive::INCLUDE_NAME === $directiveNode->name->value) { - /** @var array{if: bool} $includeArguments */ $includeArguments = Values::getArgumentValues( Directive::includeDirective(), $directiveNode, $variableValues ); + assert(is_bool($includeArguments['if']), 'ensured by query validation'); return ! $includeArguments['if']; } if (Directive::SKIP_NAME === $directiveNode->name->value) { - /** @var array{if: bool} $skipArguments */ $skipArguments = Values::getArgumentValues( Directive::skipDirective(), $directiveNode, $variableValues ); + assert(is_bool($skipArguments['if']), 'ensured by query validation'); return $skipArguments['if']; } diff --git a/src/Validator/Rules/UniqueArgumentNames.php b/src/Validator/Rules/UniqueArgumentNames.php index 27f93779d..fd676d97c 100644 --- a/src/Validator/Rules/UniqueArgumentNames.php +++ b/src/Validator/Rules/UniqueArgumentNames.php @@ -48,7 +48,7 @@ public function getASTVisitor(ASTValidationContext $context): array }, NodeKind::ARGUMENT => function (ArgumentNode $node) use ($context): VisitorOperation { $argName = $node->name->value; - if ($this->knownArgNames[$argName] ?? false) { + if (isset($this->knownArgNames[$argName])) { $context->reportError(new Error( static::duplicateArgMessage($argName), [$this->knownArgNames[$argName], $node->name] diff --git a/src/Validator/Rules/UniqueDirectivesPerLocation.php b/src/Validator/Rules/UniqueDirectivesPerLocation.php index b378f8358..f335b0346 100644 --- a/src/Validator/Rules/UniqueDirectivesPerLocation.php +++ b/src/Validator/Rules/UniqueDirectivesPerLocation.php @@ -6,7 +6,6 @@ use GraphQL\Error\Error; use GraphQL\Language\AST\DirectiveDefinitionNode; -use GraphQL\Language\AST\DirectiveNode; use GraphQL\Language\AST\Node; use GraphQL\Language\Visitor; use GraphQL\Type\Definition\Directive; @@ -66,7 +65,6 @@ public function getASTVisitor(ASTValidationContext $context): array $knownDirectives = []; - /** @var DirectiveNode $directive */ foreach ($node->directives as $directive) { $directiveName = $directive->name->value; diff --git a/src/Validator/Rules/UniqueOperationNames.php b/src/Validator/Rules/UniqueOperationNames.php index e9f17ba9b..24dc26887 100644 --- a/src/Validator/Rules/UniqueOperationNames.php +++ b/src/Validator/Rules/UniqueOperationNames.php @@ -14,7 +14,7 @@ class UniqueOperationNames extends ValidationRule { - /** @var NameNode[] */ + /** @var array */ protected array $knownOperationNames; public function getVisitor(ValidationContext $context): array diff --git a/src/Validator/Rules/ValuesOfCorrectType.php b/src/Validator/Rules/ValuesOfCorrectType.php index d52fbb3c0..e87c4b0c0 100644 --- a/src/Validator/Rules/ValuesOfCorrectType.php +++ b/src/Validator/Rules/ValuesOfCorrectType.php @@ -5,12 +5,10 @@ namespace GraphQL\Validator\Rules; use function array_keys; -use function array_map; use function count; use GraphQL\Error\Error; use GraphQL\Language\AST\BooleanValueNode; use GraphQL\Language\AST\EnumValueNode; -use GraphQL\Language\AST\FieldNode; use GraphQL\Language\AST\FloatValueNode; use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\ListValueNode; @@ -24,13 +22,10 @@ use GraphQL\Language\Printer; use GraphQL\Language\Visitor; use GraphQL\Language\VisitorOperation; -use GraphQL\Type\Definition\EnumType; -use GraphQL\Type\Definition\EnumValueDefinition; use GraphQL\Type\Definition\InputObjectType; -use GraphQL\Type\Definition\InputType; +use GraphQL\Type\Definition\LeafType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; -use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Utils\Utils; use GraphQL\Validator\ValidationContext; @@ -46,28 +41,21 @@ class ValuesOfCorrectType extends ValidationRule { public function getVisitor(ValidationContext $context): array { - $fieldName = ''; - return [ - NodeKind::FIELD => [ - 'enter' => static function (FieldNode $node) use (&$fieldName): void { - $fieldName = $node->name->value; - }, - ], - NodeKind::NULL => static function (NullValueNode $node) use ($context, &$fieldName): void { + NodeKind::NULL => static function (NullValueNode $node) use ($context): void { $type = $context->getInputType(); - if (! ($type instanceof NonNull)) { - return; + if ($type instanceof NonNull) { + $typeStr = Utils::printSafe($type); + $nodeStr = Printer::doPrint($node); + $context->reportError( + new Error( + "Expected value of type \"{$typeStr}\", found {$nodeStr}.", + $node + ) + ); } - - $context->reportError( - new Error( - static::getBadValueMessage((string) $type, Printer::doPrint($node), null, $context, $fieldName), - $node - ) - ); }, - NodeKind::LST => function (ListValueNode $node) use ($context, &$fieldName): ?VisitorOperation { + NodeKind::LST => function (ListValueNode $node) use ($context): ?VisitorOperation { // Note: TypeInfo will traverse into a list's item type, so look to the // parent input type to check if it is a list. $parentType = $context->getParentInputType(); @@ -75,19 +63,17 @@ public function getVisitor(ValidationContext $context): array ? null : Type::getNullableType($parentType); if (! $type instanceof ListOfType) { - $this->isValidScalar($context, $node, $fieldName); + $this->isValidValueNode($context, $node); return Visitor::skipNode(); } return null; }, - NodeKind::OBJECT => function (ObjectValueNode $node) use ($context, &$fieldName): ?VisitorOperation { - // Note: TypeInfo will traverse into a list's item type, so look to the - // parent input type to check if it is a list. + NodeKind::OBJECT => function (ObjectValueNode $node) use ($context): ?VisitorOperation { $type = Type::getNamedType($context->getInputType()); if (! $type instanceof InputObjectType) { - $this->isValidScalar($context, $node, $fieldName); + $this->isValidValueNode($context, $node); return Visitor::skipNode(); } @@ -101,14 +87,14 @@ public function getVisitor(ValidationContext $context): array } foreach ($inputFields as $inputFieldName => $fieldDef) { - $fieldType = $fieldDef->getType(); if (isset($fieldNodeMap[$inputFieldName]) || ! $fieldDef->isRequired()) { continue; } + $fieldType = Utils::printSafe($fieldDef->getType()); $context->reportError( new Error( - static::requiredFieldMessage($type->name, $inputFieldName, (string) $fieldType), + "Field {$type->name}.{$inputFieldName} of required type {$fieldType} was not provided.", $node ) ); @@ -131,83 +117,53 @@ public function getVisitor(ValidationContext $context): array array_keys($parentType->getFields()) ); $didYouMean = count($suggestions) > 0 - ? 'Did you mean ' . Utils::orList($suggestions) . '?' + ? ' Did you mean ' . Utils::quotedOrList($suggestions) . '?' : null; $context->reportError( new Error( - static::unknownFieldMessage($parentType->name, $node->name->value, $didYouMean), + "Field \"{$node->name->value}\" is not defined by type \"{$parentType->name}\".{$didYouMean}", $node ) ); }, - NodeKind::ENUM => function (EnumValueNode $node) use ($context, &$fieldName): void { - $type = Type::getNamedType($context->getInputType()); - if (! $type instanceof EnumType) { - $this->isValidScalar($context, $node, $fieldName); - } elseif (null === $type->getValue($node->value)) { - $context->reportError( - new Error( - static::getBadValueMessage( - $type->name, - Printer::doPrint($node), - $this->enumTypeSuggestion($type, $node), - $context, - $fieldName - ), - $node - ) - ); - } + NodeKind::ENUM => function (EnumValueNode $node) use ($context): void { + $this->isValidValueNode($context, $node); }, - NodeKind::INT => function (IntValueNode $node) use ($context, &$fieldName): void { - $this->isValidScalar($context, $node, $fieldName); + NodeKind::INT => function (IntValueNode $node) use ($context): void { + $this->isValidValueNode($context, $node); }, - NodeKind::FLOAT => function (FloatValueNode $node) use ($context, &$fieldName): void { - $this->isValidScalar($context, $node, $fieldName); + NodeKind::FLOAT => function (FloatValueNode $node) use ($context): void { + $this->isValidValueNode($context, $node); }, - NodeKind::STRING => function (StringValueNode $node) use ($context, &$fieldName): void { - $this->isValidScalar($context, $node, $fieldName); + NodeKind::STRING => function (StringValueNode $node) use ($context): void { + $this->isValidValueNode($context, $node); }, - NodeKind::BOOLEAN => function (BooleanValueNode $node) use ($context, &$fieldName): void { - $this->isValidScalar($context, $node, $fieldName); + NodeKind::BOOLEAN => function (BooleanValueNode $node) use ($context): void { + $this->isValidValueNode($context, $node); }, ]; } - public static function badValueMessage(string $typeName, string $valueName, ?string $message = null): string - { - return "Expected type {$typeName}, found {$valueName}" - . (null !== $message && '' !== $message - ? "; ${message}" - : '.'); - } - /** * @param VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode $node */ - protected function isValidScalar(ValidationContext $context, ValueNode $node, string $fieldName): void + protected function isValidValueNode(ValidationContext $context, ValueNode $node): void { // Report any error at the full type expected by the location. - /** @var ScalarType|EnumType|InputObjectType|ListOfType|NonNull|null $locationType */ $locationType = $context->getInputType(); - if (null === $locationType) { return; } $type = Type::getNamedType($locationType); - if (! $type instanceof ScalarType) { + if (! $type instanceof LeafType) { + $typeStr = Utils::printSafe($type); + $nodeStr = Printer::doPrint($node); $context->reportError( new Error( - static::getBadValueMessage( - (string) $locationType, - Printer::doPrint($node), - $this->enumTypeSuggestion($type, $node), - $context, - $fieldName - ), + "Expected value of type \"{$typeStr}\", found {$nodeStr}.", $node ) ); @@ -220,87 +176,22 @@ protected function isValidScalar(ValidationContext $context, ValueNode $node, st try { $type->parseLiteral($node); } catch (Throwable $error) { - // Ensure a reference to the original error is maintained. - $context->reportError( - new Error( - static::getBadValueMessage( - (string) $locationType, - Printer::doPrint($node), - $error->getMessage(), - $context, - $fieldName - ), - $node, - null, - [], - null, - $error - ) - ); - } - } - - /** - * @param VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode $node - */ - protected function enumTypeSuggestion(Type $type, ValueNode $node): ?string - { - if ($type instanceof EnumType) { - $suggestions = Utils::suggestionList( - Printer::doPrint($node), - array_map( - static fn (EnumValueDefinition $value): string => $value->name, - $type->getValues() - ) - ); - - return [] === $suggestions - ? null - : 'Did you mean the enum value ' . Utils::orList($suggestions) . '?'; - } - - return null; - } - - public static function badArgumentValueMessage(string $typeName, string $valueName, string $fieldName, string $argName, ?string $message = null): string - { - return "Field \"{$fieldName}\" argument \"{$argName}\" requires type {$typeName}, found {$valueName}" - . ( - null !== $message && '' !== $message - ? "; {$message}" - : '.' - ); - } - - public static function requiredFieldMessage(string $typeName, string $fieldName, string $fieldTypeName): string - { - return "Field {$typeName}.{$fieldName} of required type {$fieldTypeName} was not provided."; - } - - public static function unknownFieldMessage(string $typeName, string $fieldName, ?string $message = null): string - { - return "Field \"{$fieldName}\" is not defined by type {$typeName}" - . ( - null !== $message && '' !== $message - ? "; {$message}" - : '.' - ); - } - - protected static function getBadValueMessage( - string $typeName, - string $valueName, - ?string $message = null, - ?ValidationContext $context = null, - ?string $fieldName = null - ): string { - if (null !== $context) { - $arg = $context->getArgument(); - if (null !== $arg) { - return static::badArgumentValueMessage($typeName, $valueName, $fieldName, $arg->name, $message); + if ($error instanceof Error) { + $context->reportError($error); + } else { + $typeStr = Utils::printSafe($type); + $nodeStr = Printer::doPrint($node); + $context->reportError( + new Error( + "Expected value of type \"{$typeStr}\", found {$nodeStr}; " . $error->getMessage(), + $node, + null, + [], + null, + $error // Ensure a reference to the original error is maintained. + ) + ); } } - - return static::badValueMessage($typeName, $valueName, $message); } } diff --git a/src/Validator/ValidationContext.php b/src/Validator/ValidationContext.php index b2f838a6f..3691dc339 100644 --- a/src/Validator/ValidationContext.php +++ b/src/Validator/ValidationContext.php @@ -36,7 +36,6 @@ * allowing access to commonly useful contextual information from within a * validation rule. * - * @phpstan-import-type InputTypeAlias from InputType * @phpstan-type VariableUsage array{node: VariableNode, type: (Type&InputType)|null, defaultValue: mixed} */ class ValidationContext extends ASTValidationContext @@ -92,7 +91,7 @@ public function getRecursiveVariableUsages(OperationDefinitionNode $operation): } /** - * @param HasSelectionSet &Node $node + * @param HasSelectionSet&Node $node * * @phpstan-return array */ @@ -228,7 +227,7 @@ public function getType(): ?OutputType } /** - * @return (CompositeType & Type) | null + * @return (CompositeType&Type)|null */ public function getParentType(): ?CompositeType { @@ -236,8 +235,7 @@ public function getParentType(): ?CompositeType } /** - * @return (Type & InputType) | null - * @phpstan-return InputTypeAlias + * @return (Type&InputType)|null */ public function getInputType(): ?InputType { diff --git a/tests/Error/ErrorTest.php b/tests/Error/ErrorTest.php index 40774c85d..c53b2dfc8 100644 --- a/tests/Error/ErrorTest.php +++ b/tests/Error/ErrorTest.php @@ -4,7 +4,6 @@ namespace GraphQL\Tests\Error; -use function array_merge; use Exception; use GraphQL\Error\Error; use GraphQL\Error\FormattedError; @@ -42,7 +41,7 @@ public function testConvertsNodesToPositionsAndLocations(): void $fieldNode = $operationDefinition->selectionSet->selections[0]; $e = new Error('msg', [$fieldNode]); - self::assertEquals([$fieldNode], $e->nodes); + self::assertEquals([$fieldNode], $e->getNodes()); self::assertEquals($source, $e->getSource()); self::assertEquals([8], $e->getPositions()); self::assertEquals([new SourceLocation(2, 7)], $e->getLocations()); @@ -62,7 +61,7 @@ public function testConvertSingleNodeToPositionsAndLocations(): void $fieldNode = $operationDefinition->selectionSet->selections[0]; $e = new Error('msg', $fieldNode); // Non-array value. - self::assertEquals([$fieldNode], $e->nodes); + self::assertEquals([$fieldNode], $e->getNodes()); self::assertEquals($source, $e->getSource()); self::assertEquals([8], $e->getPositions()); self::assertEquals([new SourceLocation(2, 7)], $e->getLocations()); @@ -80,7 +79,7 @@ public function testConvertsNodeWithStart0ToPositionsAndLocations(): void $operationNode = $ast->definitions[0]; $e = new Error('msg', [$operationNode]); - self::assertEquals([$operationNode], $e->nodes); + self::assertEquals([$operationNode], $e->getNodes()); self::assertEquals($source, $e->getSource()); self::assertEquals([0], $e->getPositions()); self::assertEquals([new SourceLocation(1, 1)], $e->getLocations()); @@ -96,7 +95,7 @@ public function testConvertsSourceAndPositionsToLocations(): void }'); $e = new Error('msg', null, $source, [10]); - self::assertEquals(null, $e->nodes); + self::assertEquals(null, $e->getNodes()); self::assertEquals($source, $e->getSource()); self::assertEquals([10], $e->getPositions()); self::assertEquals([new SourceLocation(2, 9)], $e->getLocations()); @@ -175,7 +174,10 @@ public function testErrorReadsOverridenMethods(): void $error = new class('msg', null, null, [], null, null, ['foo' => 'bar']) extends Error { public function getExtensions(): ?array { - return array_merge(parent::getExtensions(), ['subfoo' => 'subbar']); + $extensions = parent::getExtensions(); + $extensions['subfoo'] = 'subbar'; + + return $extensions; } public function getPositions(): array diff --git a/tests/Executor/DeferredFieldsTest.php b/tests/Executor/DeferredFieldsTest.php index c933a37a3..c7d014c8e 100644 --- a/tests/Executor/DeferredFieldsTest.php +++ b/tests/Executor/DeferredFieldsTest.php @@ -14,8 +14,8 @@ use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; use function in_array; -use function json_encode; use PHPUnit\Framework\TestCase; +use function Safe\json_encode; class DeferredFieldsTest extends TestCase { diff --git a/tests/Executor/ExecutorLazySchemaTest.php b/tests/Executor/ExecutorLazySchemaTest.php index 0f91d0d04..8e9903c71 100644 --- a/tests/Executor/ExecutorLazySchemaTest.php +++ b/tests/Executor/ExecutorLazySchemaTest.php @@ -277,8 +277,11 @@ public function loadType(string $name, bool $isExecutorCall = false): ?Type 'interfaces' => function (): array { $this->calls[] = 'SomeObject.interfaces'; + /** @var InterfaceType $someInterface */ + $someInterface = $this->loadType('SomeInterface'); + return [ - $this->loadType('SomeInterface'), + $someInterface, ]; }, ]); @@ -289,9 +292,14 @@ public function loadType(string $name, bool $isExecutorCall = false): ?Type 'fields' => function (): array { $this->calls[] = 'OtherObject.fields'; + /** @var UnionType $someUnion */ + $someUnion = $this->loadType('SomeUnion'); + /** @var InterfaceType $someInterface */ + $someInterface = $this->loadType('SomeInterface'); + return [ - 'union' => ['type' => $this->loadType('SomeUnion')], - 'iface' => ['type' => Type::nonNull($this->loadType('SomeInterface'))], + 'union' => ['type' => $someUnion], + 'iface' => ['type' => Type::nonNull($someInterface)], ]; }, ]); @@ -318,12 +326,18 @@ public function loadType(string $name, bool $isExecutorCall = false): ?Type 'resolveType' => function () { $this->calls[] = 'SomeUnion.resolveType'; - return $this->loadType('DeeperObject'); + /** @var ObjectType $deeperObject */ + $deeperObject = $this->loadType('DeeperObject'); + + return $deeperObject; }, 'types' => function (): array { $this->calls[] = 'SomeUnion.types'; - return [$this->loadType('DeeperObject')]; + /** @var ObjectType $deeperObject */ + $deeperObject = $this->loadType('DeeperObject'); + + return [$deeperObject]; }, ]); @@ -333,7 +347,10 @@ public function loadType(string $name, bool $isExecutorCall = false): ?Type 'resolveType' => function () { $this->calls[] = 'SomeInterface.resolveType'; - return $this->loadType('SomeObject'); + /** @var ObjectType $someObject */ + $someObject = $this->loadType('SomeObject'); + + return $someObject; }, 'fields' => function (): array { $this->calls[] = 'SomeInterface.fields'; diff --git a/tests/Executor/ExecutorTest.php b/tests/Executor/ExecutorTest.php index da508c2be..4441e76b3 100644 --- a/tests/Executor/ExecutorTest.php +++ b/tests/Executor/ExecutorTest.php @@ -25,9 +25,9 @@ use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; -use function json_encode; use PHPUnit\Framework\TestCase; use ReturnTypeWillChange; +use function Safe\json_encode; use stdClass; class ExecutorTest extends TestCase diff --git a/tests/Executor/ListsTest.php b/tests/Executor/ListsTest.php index 5346769e1..3751c6024 100644 --- a/tests/Executor/ListsTest.php +++ b/tests/Executor/ListsTest.php @@ -60,8 +60,8 @@ private function checkHandlesNullableLists($testData, array $expected): void } /** - * @param Type &OutputType $testType - * @param mixed $testData + * @param Type&OutputType $testType + * @param mixed $testData * @param array $expected */ private function check(Type $testType, $testData, array $expected, int $debug = DebugFlag::NONE): void diff --git a/tests/Executor/NonNullTest.php b/tests/Executor/NonNullTest.php index 35563ccdf..2e4d563ae 100644 --- a/tests/Executor/NonNullTest.php +++ b/tests/Executor/NonNullTest.php @@ -18,37 +18,31 @@ use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; use function is_string; -use function json_encode; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; +use function Safe\json_encode; class NonNullTest extends TestCase { use ArraySubsetAsserts; - /** @var Exception */ - public $syncError; + public Exception $syncError; - /** @var Exception */ - public $syncNonNullError; + public Exception $syncNonNullError; - /** @var Exception */ - public $promiseError; + public Exception $promiseError; - /** @var Exception */ - public $promiseNonNullError; + public Exception $promiseNonNullError; - /** @var callable[] */ - public $throwingData; + /** @var array */ + public array $throwingData; - /** @var callable[] */ - public $nullingData; + /** @var array */ + public array $nullingData; - /** @var Schema */ - public $schema; + public Schema $schema; - /** @var Schema */ - public $schemaWithNonNullArg; + public Schema $schemaWithNonNullArg; public function setUp(): void { @@ -361,55 +355,55 @@ public function testNullsAComplexTreeOfNullableFieldsThatThrow(): void $ast = Parser::parse($doc); - $expected = [ - 'data' => [ + $expectedData = [ + 'syncNest' => [ + 'sync' => null, + 'promise' => null, 'syncNest' => [ 'sync' => null, 'promise' => null, - 'syncNest' => [ - 'sync' => null, - 'promise' => null, - ], - 'promiseNest' => [ - 'sync' => null, - 'promise' => null, - ], ], 'promiseNest' => [ 'sync' => null, 'promise' => null, - 'syncNest' => [ - 'sync' => null, - 'promise' => null, - ], - 'promiseNest' => [ - 'sync' => null, - 'promise' => null, - ], ], ], - 'errors' => [ - ErrorHelper::create($this->syncError->getMessage(), [new SourceLocation(4, 11)]), - ErrorHelper::create($this->syncError->getMessage(), [new SourceLocation(7, 13)]), - ErrorHelper::create($this->syncError->getMessage(), [new SourceLocation(11, 13)]), - ErrorHelper::create($this->syncError->getMessage(), [new SourceLocation(16, 11)]), - ErrorHelper::create($this->syncError->getMessage(), [new SourceLocation(19, 13)]), - ErrorHelper::create($this->promiseError->getMessage(), [new SourceLocation(5, 11)]), - ErrorHelper::create($this->promiseError->getMessage(), [new SourceLocation(8, 13)]), - ErrorHelper::create($this->syncError->getMessage(), [new SourceLocation(23, 13)]), - ErrorHelper::create($this->promiseError->getMessage(), [new SourceLocation(12, 13)]), - ErrorHelper::create($this->promiseError->getMessage(), [new SourceLocation(17, 11)]), - ErrorHelper::create($this->promiseError->getMessage(), [new SourceLocation(20, 13)]), - ErrorHelper::create($this->promiseError->getMessage(), [new SourceLocation(24, 13)]), + 'promiseNest' => [ + 'sync' => null, + 'promise' => null, + 'syncNest' => [ + 'sync' => null, + 'promise' => null, + ], + 'promiseNest' => [ + 'sync' => null, + 'promise' => null, + ], ], ]; + $expectedErrors = [ + ErrorHelper::create($this->syncError->getMessage(), [new SourceLocation(4, 11)]), + ErrorHelper::create($this->syncError->getMessage(), [new SourceLocation(7, 13)]), + ErrorHelper::create($this->syncError->getMessage(), [new SourceLocation(11, 13)]), + ErrorHelper::create($this->syncError->getMessage(), [new SourceLocation(16, 11)]), + ErrorHelper::create($this->syncError->getMessage(), [new SourceLocation(19, 13)]), + ErrorHelper::create($this->promiseError->getMessage(), [new SourceLocation(5, 11)]), + ErrorHelper::create($this->promiseError->getMessage(), [new SourceLocation(8, 13)]), + ErrorHelper::create($this->syncError->getMessage(), [new SourceLocation(23, 13)]), + ErrorHelper::create($this->promiseError->getMessage(), [new SourceLocation(12, 13)]), + ErrorHelper::create($this->promiseError->getMessage(), [new SourceLocation(17, 11)]), + ErrorHelper::create($this->promiseError->getMessage(), [new SourceLocation(20, 13)]), + ErrorHelper::create($this->promiseError->getMessage(), [new SourceLocation(24, 13)]), + ]; $result = Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q')->toArray(); - self::assertEquals($expected['data'], $result['data']); + self::assertArrayHasKey('data', $result); + self::assertEquals($expectedData, $result['data']); - self::assertCount(count($expected['errors']), $result['errors']); - foreach ($expected['errors'] as $expectedError) { + self::assertArrayHasKey('errors', $result); + self::assertCount(count($expectedErrors), $result['errors']); + foreach ($expectedErrors as $expectedError) { $found = false; foreach ($result['errors'] as $error) { try { diff --git a/tests/Executor/Promise/SyncPromiseAdapterTest.php b/tests/Executor/Promise/SyncPromiseAdapterTest.php index 8c6bc22e3..bc324e652 100644 --- a/tests/Executor/Promise/SyncPromiseAdapterTest.php +++ b/tests/Executor/Promise/SyncPromiseAdapterTest.php @@ -50,7 +50,6 @@ public function testConvert(): void $this->expectException(InvariantViolation::class); $this->expectExceptionMessage('Expected instance of GraphQL\Deferred, got (empty string)'); - // @phpstan-ignore-next-line purposefully wrong $this->promises->convertThenable(''); } @@ -216,8 +215,15 @@ public function testWait(): void // Having single promise queue means that we won't stop in wait // until all pending promises are resolved self::assertEquals(2, $result); - self::assertEquals(SyncPromise::FULFILLED, $p3->adoptedPromise->state); - self::assertEquals(SyncPromise::FULFILLED, $all->adoptedPromise->state); + + $p3AdoptedPromise = $p3->adoptedPromise; + self::assertInstanceOf(SyncPromise::class, $p3AdoptedPromise); + self::assertEquals(SyncPromise::FULFILLED, $p3AdoptedPromise->state); + + $allAdoptedPromise = $all->adoptedPromise; + self::assertInstanceOf(SyncPromise::class, $allAdoptedPromise); + self::assertEquals(SyncPromise::FULFILLED, $allAdoptedPromise->state); + self::assertEquals([1, 2, 3, 4], $called); $expectedResult = [0, 1, 2, 3, 4]; diff --git a/tests/Executor/ResolveTest.php b/tests/Executor/ResolveTest.php index 97f80a952..2d54ec7f6 100644 --- a/tests/Executor/ResolveTest.php +++ b/tests/Executor/ResolveTest.php @@ -10,8 +10,8 @@ use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; -use function json_encode; use PHPUnit\Framework\TestCase; +use function Safe\json_encode; use function uniqid; /** diff --git a/tests/Executor/TestClasses/ComplexScalar.php b/tests/Executor/TestClasses/ComplexScalar.php index d6d6b5750..d7cb462fb 100644 --- a/tests/Executor/TestClasses/ComplexScalar.php +++ b/tests/Executor/TestClasses/ComplexScalar.php @@ -39,10 +39,14 @@ public function parseValue($value): string public function parseLiteral(Node $valueNode, ?array $variables = null): string { - if ('SerializedValue' === $valueNode->value) { + $value = property_exists($valueNode, 'value') + ? $valueNode->value + : null; + + if ('SerializedValue' === $value) { return 'DeserializedValue'; } - throw new Error('Cannot represent literal as ComplexScalar: ' . Utils::printSafe($valueNode->value)); + throw new Error('Cannot represent literal as ComplexScalar: ' . Utils::printSafe($value)); } } diff --git a/tests/Executor/UnionInterfaceTest.php b/tests/Executor/UnionInterfaceTest.php index e11ea5c4e..7025160bd 100644 --- a/tests/Executor/UnionInterfaceTest.php +++ b/tests/Executor/UnionInterfaceTest.php @@ -4,8 +4,8 @@ namespace GraphQL\Tests\Executor; -use GraphQL\Error\DebugFlag; use GraphQL\Error\InvariantViolation; +use GraphQL\Executor\ExecutionResult; use GraphQL\Executor\Executor; use GraphQL\GraphQL; use GraphQL\Language\Parser; @@ -147,6 +147,16 @@ public function setUp(): void $this->john = new Person('John', [$this->garfield, $this->odie], [$this->liz, $this->odie]); } + /** + * @param array $expected + * @param mixed $result + */ + protected static function assertExecutionResultEquals(array $expected, $result): void + { + self::assertInstanceOf(ExecutionResult::class, $result); + self::assertEquals($expected, $result->toArray()); + } + // Execute: Union and intersection types /** @@ -236,7 +246,7 @@ enumValues { name } ], ], ]; - self::assertEquals($expected, Executor::execute($this->schema, $ast)->toArray()); + self::assertExecutionResultEquals($expected, Executor::execute($this->schema, $ast)); } /** @@ -276,7 +286,7 @@ public function testExecutesUsingUnionTypes(): void ], ]; - self::assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray()); + self::assertExecutionResultEquals($expected, Executor::execute($this->schema, $ast, $this->john)); } /** @@ -320,7 +330,7 @@ public function testExecutesUnionTypesWithInlineFragments(): void ], ], ]; - self::assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray()); + self::assertExecutionResultEquals($expected, Executor::execute($this->schema, $ast, $this->john)); } /** @@ -352,7 +362,7 @@ public function testExecutesUsingInterfaceTypes(): void ], ]; - self::assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray()); + self::assertExecutionResultEquals($expected, Executor::execute($this->schema, $ast, $this->john)); } /** @@ -415,7 +425,7 @@ public function testExecutesInterfaceTypesWithInlineFragments(): void ], ]; - self::assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE)); + self::assertExecutionResultEquals($expected, Executor::execute($this->schema, $ast, $this->john)); } /** @@ -507,8 +517,7 @@ public function testAllowsFragmentConditionsToBeAbstractTypes(): void ], ], ]; - - self::assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray(DebugFlag::RETHROW_INTERNAL_EXCEPTIONS)); + self::assertExecutionResultEquals($expected, Executor::execute($this->schema, $ast, $this->john)); } /** diff --git a/tests/Language/AST/NodeTest.php b/tests/Language/AST/NodeTest.php index 23d99a069..6206744b7 100644 --- a/tests/Language/AST/NodeTest.php +++ b/tests/Language/AST/NodeTest.php @@ -10,8 +10,8 @@ use GraphQL\Language\Parser; use GraphQL\Utils\AST; use function json_decode; -use function json_encode; use PHPUnit\Framework\TestCase; +use function Safe\json_encode; class NodeTest extends TestCase { diff --git a/tests/Language/NodeListTest.php b/tests/Language/NodeListTest.php index b30f5ec7a..d6a4fa075 100644 --- a/tests/Language/NodeListTest.php +++ b/tests/Language/NodeListTest.php @@ -21,6 +21,27 @@ public function testConvertArrayToASTNode(): void self::assertEquals($nameNode, $nodeList[$key]); } + public function testCloneDeep(): void + { + $nameNode = new NameNode(['value' => 'bar']); + + $nodeList = new NodeList([ + 'array' => $nameNode->toArray(), + 'instance' => $nameNode, + ]); + + $cloned = $nodeList->cloneDeep(); + + self::assertNotSameButEquals($nodeList['array'], $cloned['array']); + self::assertNotSameButEquals($nodeList['instance'], $cloned['instance']); + } + + private static function assertNotSameButEquals(object $node, object $clone): void + { + self::assertNotSame($node, $clone); + self::assertEquals($node, $clone); + } + public function testThrowsOnInvalidArrays(): void { $nodeList = new NodeList([]); diff --git a/tests/Language/ParserTest.php b/tests/Language/ParserTest.php index 89492c3e4..e67647e7c 100644 --- a/tests/Language/ParserTest.php +++ b/tests/Language/ParserTest.php @@ -4,7 +4,6 @@ namespace GraphQL\Tests\Language; -use function file_get_contents; use GraphQL\Error\SyntaxError; use GraphQL\Language\AST\ArgumentNode; use GraphQL\Language\AST\FieldNode; @@ -21,7 +20,7 @@ use GraphQL\Tests\TestCaseBase; use GraphQL\Utils\Utils; use function is_array; -use function sprintf; +use function Safe\file_get_contents; class ParserTest extends TestCaseBase { @@ -210,9 +209,9 @@ public function testParsesMultiByteCharacters(): void 'arguments' => new NodeList([ new ArgumentNode([ 'name' => new NameNode(['value' => 'arg']), - 'value' => new StringValueNode( - ['value' => sprintf('Has a %s multi-byte character.', $char)] - ), + 'value' => new StringValueNode([ + 'value' => "Has a {$char} multi-byte character.", + ]), ]), ]), 'directives' => new NodeList([]), diff --git a/tests/Language/PrinterTest.php b/tests/Language/PrinterTest.php index e465e3e2f..833b39e13 100644 --- a/tests/Language/PrinterTest.php +++ b/tests/Language/PrinterTest.php @@ -4,7 +4,6 @@ namespace GraphQL\Tests\Language; -use function file_get_contents; use GraphQL\Language\AST\FieldNode; use GraphQL\Language\AST\NameNode; use GraphQL\Language\AST\NodeList; @@ -13,6 +12,7 @@ use GraphQL\Type\Definition\Type; use GraphQL\Utils\AST; use PHPUnit\Framework\TestCase; +use function Safe\file_get_contents; class PrinterTest extends TestCase { diff --git a/tests/Language/SchemaPrinterTest.php b/tests/Language/SchemaPrinterTest.php index 8d3f76b8f..98d4f6a32 100644 --- a/tests/Language/SchemaPrinterTest.php +++ b/tests/Language/SchemaPrinterTest.php @@ -4,14 +4,14 @@ namespace GraphQL\Tests\Language; -use function file_get_contents; use GraphQL\Language\AST\NameNode; use GraphQL\Language\AST\NodeList; use GraphQL\Language\AST\ScalarTypeDefinitionNode; use GraphQL\Language\Parser; use GraphQL\Language\Printer; -use function json_encode; use PHPUnit\Framework\TestCase; +use function Safe\file_get_contents; +use function Safe\json_encode; /** * @see describe('Printer: SDL document') diff --git a/tests/Language/SerializationTest.php b/tests/Language/SerializationTest.php index aa444e418..aa4cf25f5 100644 --- a/tests/Language/SerializationTest.php +++ b/tests/Language/SerializationTest.php @@ -6,7 +6,6 @@ use function array_keys; use function count; -use function file_get_contents; use function get_class; use function get_object_vars; use GraphQL\Language\AST\Location; @@ -17,6 +16,7 @@ use function implode; use function json_decode; use PHPUnit\Framework\TestCase; +use function Safe\file_get_contents; class SerializationTest extends TestCase { @@ -40,7 +40,7 @@ public function testUnserializesAst(): void /** * Compares two nodes by actually iterating over all NodeLists, properly comparing locations (ignoring tokens), etc. * - * @param string[] $path + * @param array $path */ private static function assertNodesAreEqual(Node $expected, Node $actual, array $path = []): void { @@ -50,7 +50,6 @@ private static function assertNodesAreEqual(Node $expected, Node $actual, array $expectedVars = get_object_vars($expected); $actualVars = get_object_vars($actual); - self::assertCount(count($expectedVars), $actualVars, $err); self::assertEquals(array_keys($expectedVars), array_keys($actualVars), $err); foreach ($expectedVars as $name => $expectedValue) { diff --git a/tests/Language/VisitorTest.php b/tests/Language/VisitorTest.php index 1b3f2094a..b046ba059 100644 --- a/tests/Language/VisitorTest.php +++ b/tests/Language/VisitorTest.php @@ -8,10 +8,7 @@ use function array_pop; use function array_slice; use function count; -use function file_get_contents; use function func_get_args; -use function gettype; -use GraphQL\Language\AST\DefinitionNode; use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\FieldNode; use GraphQL\Language\AST\NameNode; @@ -29,6 +26,7 @@ use GraphQL\Type\Definition\Type; use GraphQL\Utils\TypeInfo; use function is_numeric; +use function Safe\file_get_contents; class VisitorTest extends ValidatorTestCase { @@ -129,11 +127,11 @@ private function checkVisitorFnArgs(DocumentNode $ast, array $args, bool $isEdit return; } - self::assertContains(gettype($key), ['integer', 'string']); - /** @var int|string $key */ if ($parent instanceof NodeList) { - self::assertArrayHasKey($key, $parent); + self::assertIsInt($key); + self::assertTrue(isset($parent[$key])); } else { + self::assertIsString($key); self::assertObjectHasAttribute($key, $parent); } @@ -233,10 +231,9 @@ public function testAllowsEditingRootNodeOnEnterAndLeave(): void NodeKind::DOCUMENT => [ 'enter' => function (DocumentNode $node) use ($ast): DocumentNode { $this->checkVisitorFnArgs($ast, func_get_args()); - /** @var NodeList $definitionNodeList */ - $definitionNodeList = new NodeList([]); $tmp = clone $node; - $tmp->definitions = $definitionNodeList; + // @phpstan-ignore-next-line generic type of empty NodeList is not initialized + $tmp->definitions = new NodeList([]); $tmp->didEnter = true; return $tmp; diff --git a/tests/PhpStan/Type/Definition/Type/IsAbstractTypeStaticMethodTypeSpecifyingExtension.php b/tests/PhpStan/Type/Definition/Type/IsAbstractTypeStaticMethodTypeSpecifyingExtension.php new file mode 100644 index 000000000..f623f56f9 --- /dev/null +++ b/tests/PhpStan/Type/Definition/Type/IsAbstractTypeStaticMethodTypeSpecifyingExtension.php @@ -0,0 +1,44 @@ +typeSpecifier = $typeSpecifier; + } + + public function isStaticMethodSupported(MethodReflection $staticMethodReflection, StaticCall $node, TypeSpecifierContext $context): bool + { + // The $context argument tells us if we're in an if condition or not (as in this case). + return 'isAbstractType' === $staticMethodReflection->getName() && ! $context->null(); + } + + public function specifyTypes(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + return $this->typeSpecifier->create($node->getArgs()[0]->value, new ObjectType(AbstractType::class), $context); + } +} diff --git a/tests/PhpStan/Type/Definition/Type/IsCompositeTypeStaticMethodTypeSpecifyingExtension.php b/tests/PhpStan/Type/Definition/Type/IsCompositeTypeStaticMethodTypeSpecifyingExtension.php index bdad1f404..71e33cd8c 100644 --- a/tests/PhpStan/Type/Definition/Type/IsCompositeTypeStaticMethodTypeSpecifyingExtension.php +++ b/tests/PhpStan/Type/Definition/Type/IsCompositeTypeStaticMethodTypeSpecifyingExtension.php @@ -39,6 +39,6 @@ public function isStaticMethodSupported(MethodReflection $staticMethodReflection public function specifyTypes(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - return $this->typeSpecifier->create($node->args[0]->value, new ObjectType(CompositeType::class), $context); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new ObjectType(CompositeType::class), $context); } } diff --git a/tests/PhpStan/Type/Definition/Type/IsInputTypeStaticMethodTypeSpecifyingExtension.php b/tests/PhpStan/Type/Definition/Type/IsInputTypeStaticMethodTypeSpecifyingExtension.php index c8a1eb6f9..4668bfe7d 100644 --- a/tests/PhpStan/Type/Definition/Type/IsInputTypeStaticMethodTypeSpecifyingExtension.php +++ b/tests/PhpStan/Type/Definition/Type/IsInputTypeStaticMethodTypeSpecifyingExtension.php @@ -39,6 +39,6 @@ public function isStaticMethodSupported(MethodReflection $staticMethodReflection public function specifyTypes(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - return $this->typeSpecifier->create($node->args[0]->value, new ObjectType(InputType::class), $context); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new ObjectType(InputType::class), $context); } } diff --git a/tests/PhpStan/Type/Definition/Type/IsOutputTypeStaticMethodTypeSpecifyingExtension.php b/tests/PhpStan/Type/Definition/Type/IsOutputTypeStaticMethodTypeSpecifyingExtension.php index afc8b0e35..b7283c9a9 100644 --- a/tests/PhpStan/Type/Definition/Type/IsOutputTypeStaticMethodTypeSpecifyingExtension.php +++ b/tests/PhpStan/Type/Definition/Type/IsOutputTypeStaticMethodTypeSpecifyingExtension.php @@ -39,6 +39,6 @@ public function isStaticMethodSupported(MethodReflection $staticMethodReflection public function specifyTypes(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - return $this->typeSpecifier->create($node->args[0]->value, new ObjectType(OutputType::class), $context); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new ObjectType(OutputType::class), $context); } } diff --git a/tests/Regression/Issue396Test.php b/tests/Regression/Issue396Test.php index 94a6d9518..98ee80e1b 100644 --- a/tests/Regression/Issue396Test.php +++ b/tests/Regression/Issue396Test.php @@ -32,15 +32,15 @@ public function testUnionResolveType(): void 'types' => [$a, $b, $c], 'resolveType' => static function ($result, $value, ResolveInfo $info) use ($a, $b, $c, &$log): ?Type { $log[] = [$result, $info->path]; - if (stristr($result['name'], 'A')) { + if (false !== stristr($result['name'], 'A')) { return $a; } - if (stristr($result['name'], 'B')) { + if (false !== stristr($result['name'], 'B')) { return $b; } - if (stristr($result['name'], 'C')) { + if (false !== stristr($result['name'], 'C')) { return $c; } @@ -103,15 +103,15 @@ public function testInterfaceResolveType(): void ], 'resolveType' => static function ($result, $value, ResolveInfo $info) use (&$a, &$b, &$c, &$log): ?ObjectType { $log[] = [$result, $info->path]; - if (stristr($result['name'], 'A')) { + if (false !== stristr($result['name'], 'A')) { return $a; } - if (stristr($result['name'], 'B')) { + if (false !== stristr($result['name'], 'B')) { return $b; } - if (stristr($result['name'], 'C')) { + if (false !== stristr($result['name'], 'C')) { return $c; } @@ -119,9 +119,24 @@ public function testInterfaceResolveType(): void }, ]); - $a = new ObjectType(['name' => 'A', 'fields' => ['name' => Type::string()], 'interfaces' => [$interfaceResult]]); - $b = new ObjectType(['name' => 'B', 'fields' => ['name' => Type::string()], 'interfaces' => [$interfaceResult]]); - $c = new ObjectType(['name' => 'C', 'fields' => ['name' => Type::string()], 'interfaces' => [$interfaceResult]]); + $a = new ObjectType( + [ + 'name' => 'A', + 'fields' => ['name' => Type::string()], + 'interfaces' => [$interfaceResult], ] + ); + $b = new ObjectType( + [ + 'name' => 'B', + 'fields' => ['name' => Type::string()], + 'interfaces' => [$interfaceResult], ] + ); + $c = new ObjectType( + [ + 'name' => 'C', + 'fields' => ['name' => Type::string()], + 'interfaces' => [$interfaceResult], ] + ); $exampleType = new ObjectType([ 'name' => 'Example', @@ -154,11 +169,13 @@ public function testInterfaceResolveType(): void GraphQL::executeQuery($schema, $query); - $expected = [ - [['name' => 'A 1'], ['field', 0]], - [['name' => 'B 2'], ['field', 1]], - [['name' => 'C 3'], ['field', 2]], - ]; - self::assertEquals($expected, $log); + self::assertEquals( + [ + [['name' => 'A 1'], ['field', 0]], + [['name' => 'B 2'], ['field', 1]], + [['name' => 'C 3'], ['field', 2]], + ], + $log + ); } } diff --git a/tests/Server/PsrResponseTest.php b/tests/Server/PsrResponseTest.php index 88e285759..b108db455 100644 --- a/tests/Server/PsrResponseTest.php +++ b/tests/Server/PsrResponseTest.php @@ -6,10 +6,11 @@ use GraphQL\Executor\ExecutionResult; use GraphQL\Server\Helper; -use function json_encode; use Nyholm\Psr7\Response; use Nyholm\Psr7\Stream; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ResponseInterface; +use function Safe\json_encode; final class PsrResponseTest extends TestCase { @@ -19,10 +20,10 @@ public function testConvertsResultToPsrResponse(): void $stream = Stream::create(); $psrResponse = new Response(); - $helper = new Helper(); + $response = (new Helper())->toPsrResponse($result, $psrResponse, $stream); - $resp = $helper->toPsrResponse($result, $psrResponse, $stream); - self::assertSame(json_encode($result), (string) $resp->getBody()); - self::assertSame(['Content-Type' => ['application/json']], $resp->getHeaders()); + self::assertInstanceOf(ResponseInterface::class, $response); + self::assertSame(json_encode($result), (string) $response->getBody()); + self::assertSame(['Content-Type' => ['application/json']], $response->getHeaders()); } } diff --git a/tests/Server/QueryExecutionTest.php b/tests/Server/QueryExecutionTest.php index c20285245..16148a93e 100644 --- a/tests/Server/QueryExecutionTest.php +++ b/tests/Server/QueryExecutionTest.php @@ -73,7 +73,10 @@ private function executeQuery(string $query, ?array $variables = null, bool $rea $readonly ); - return (new Helper())->executeOperation($this->config, $op); + $result = (new Helper())->executeOperation($this->config, $op); + self::assertInstanceOf(ExecutionResult::class, $result); + + return $result; } public function testReturnsSyntaxErrors(): void @@ -292,7 +295,10 @@ private function executePersistedQuery(string $queryId, ?array $variables = null 'variables' => $variables, ]); - return (new Helper())->executeOperation($this->config, $op); + $result = (new Helper())->executeOperation($this->config, $op); + self::assertInstanceOf(ExecutionResult::class, $result); + + return $result; } public function testBatchedQueriesAreDisabledByDefault(): void @@ -638,11 +644,12 @@ public function testAppliesErrorFormatter(): void { $called = false; $error = null; - $this->config->setErrorFormatter(static function ($e) use (&$called, &$error): array { + $formattedError = ['message' => 'formatted']; + $this->config->setErrorFormatter(static function ($e) use (&$called, &$error, $formattedError): array { $called = true; $error = $e; - return ['test' => 'formatted']; + return $formattedError; }); $result = $this->executeQuery('{fieldWithSafeException}'); @@ -650,7 +657,7 @@ public function testAppliesErrorFormatter(): void $formatted = $result->toArray(); $expected = [ 'errors' => [ - ['test' => 'formatted'], + $formattedError, ], ]; self::assertTrue($called); @@ -661,12 +668,14 @@ public function testAppliesErrorFormatter(): void $formatted = $result->toArray(DebugFlag::INCLUDE_TRACE); $expected = [ 'errors' => [ - [ - 'test' => 'formatted', - 'extensions' => [ - 'trace' => [], + array_merge( + $formattedError, + [ + 'extensions' => [ + 'trace' => [], + ], ], - ], + ), ], ]; self::assertArraySubset($expected, $formatted); @@ -677,14 +686,15 @@ public function testAppliesErrorsHandler(): void $called = false; $errors = null; $formatter = null; - $this->config->setErrorsHandler(static function ($e, $f) use (&$called, &$errors, &$formatter): array { + $handledErrors = [ + ['message' => 'handled'], + ]; + $this->config->setErrorsHandler(static function ($e, $f) use (&$called, &$errors, &$formatter, $handledErrors): array { $called = true; $errors = $e; $formatter = $f; - return [ - ['test' => 'handled'], - ]; + return $handledErrors; }); $result = $this->executeQuery('{fieldWithSafeException,test: fieldWithSafeException}'); @@ -692,9 +702,7 @@ public function testAppliesErrorsHandler(): void self::assertFalse($called); $formatted = $result->toArray(); $expected = [ - 'errors' => [ - ['test' => 'handled'], - ], + 'errors' => $handledErrors, ]; self::assertTrue($called); self::assertArraySubset($expected, $formatted); diff --git a/tests/Server/RequestParsingTest.php b/tests/Server/RequestParsingTest.php index ee05e62dd..88f21c0f4 100644 --- a/tests/Server/RequestParsingTest.php +++ b/tests/Server/RequestParsingTest.php @@ -10,12 +10,12 @@ use GraphQL\Server\RequestError; use function http_build_query; use InvalidArgumentException; -use function json_encode; use Nyholm\Psr7\Request; use Nyholm\Psr7\ServerRequest; use Nyholm\Psr7\Stream; use Nyholm\Psr7\Uri; use PHPUnit\Framework\TestCase; +use function Safe\json_encode; class RequestParsingTest extends TestCase { @@ -28,6 +28,7 @@ public function testParsesGraphqlRequest(): void ]; foreach ($parsed as $source => $parsedBody) { + self::assertInstanceOf(OperationParams::class, $parsedBody); self::assertValidOperationParams($parsedBody, $query, null, null, null, null, $source); self::assertFalse($parsedBody->readOnly, $source); } @@ -41,9 +42,7 @@ private function parseRawRequest(?string $contentType, string $content, string $ $_SERVER['CONTENT_TYPE'] = $contentType; $_SERVER['REQUEST_METHOD'] = $method; - $helper = new Helper(); - - return $helper->parseHttpRequest(static fn (): string => $content); + return (new Helper())->parseHttpRequest(static fn (): string => $content); } /** @@ -58,13 +57,11 @@ private function parsePsrRequest(?string $contentType, string $content, string $ Stream::create($content) ); - $helper = new Helper(); - - return $helper->parsePsrRequest($psrRequest); + return (new Helper())->parsePsrRequest($psrRequest); } /** - * @param mixed $variables + * @param mixed $variables * @param array $extensions */ private static function assertValidOperationParams( @@ -102,39 +99,36 @@ public function testParsesUrlencodedRequest(): void ]; foreach ($parsed as $method => $parsedBody) { + self::assertInstanceOf(OperationParams::class, $parsedBody); self::assertValidOperationParams($parsedBody, $query, null, $variables, $operation, null, $method); self::assertFalse($parsedBody->readOnly, $method); } } /** - * @param mixed[] $postValue + * @param array $postValue * - * @return OperationParams|OperationParams[] + * @return OperationParams|array */ - private function parseRawFormUrlencodedRequest($postValue) + private function parseRawFormUrlencodedRequest(array $postValue) { $_SERVER['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; $_SERVER['REQUEST_METHOD'] = 'POST'; $_POST = $postValue; - $helper = new Helper(); - - return $helper->parseHttpRequest(static function (): void { + return (new Helper())->parseHttpRequest(static function (): void { throw new InvariantViolation("Shouldn't read from php://input for urlencoded request"); }); } /** - * @param mixed[] $postValue + * @param array $postValue * - * @return OperationParams[]|OperationParams + * @return OperationParams|array */ - private function parsePsrFormUrlEncodedRequest($postValue) + private function parsePsrFormUrlEncodedRequest(array $postValue) { - $helper = new Helper(); - - return $helper->parsePsrRequest( + return (new Helper())->parsePsrRequest( new Request( 'POST', '', @@ -146,11 +140,11 @@ private function parsePsrFormUrlEncodedRequest($postValue) /** * @param array $postValue + * + * @return OperationParams|array */ - private function parsePsrFormUrlEncodedServerRequest(array $postValue, bool $parsed): OperationParams + private function parsePsrFormUrlEncodedServerRequest(array $postValue, bool $parsed) { - $helper = new Helper(); - $request = new ServerRequest( 'POST', '', @@ -162,7 +156,7 @@ private function parsePsrFormUrlEncodedServerRequest(array $postValue, bool $par $request = $request->withParsedBody($postValue); } - return $helper->parsePsrRequest($request); + return (new Helper())->parsePsrRequest($request); } public function testParsesGetRequest(): void @@ -182,36 +176,35 @@ public function testParsesGetRequest(): void ]; foreach ($parsed as $method => $parsedBody) { + self::assertInstanceOf(OperationParams::class, $parsedBody); self::assertValidOperationParams($parsedBody, $query, null, $variables, $operation, null, $method); self::assertTrue($parsedBody->readOnly, $method); } } /** - * @param mixed[] $getValue + * @param array $getValue + * + * @return OperationParams|array */ - private function parseRawGetRequest($getValue): OperationParams + private function parseRawGetRequest(array $getValue) { $_SERVER['REQUEST_METHOD'] = 'GET'; $_GET = $getValue; - $helper = new Helper(); - - return $helper->parseHttpRequest(static function (): void { + return (new Helper())->parseHttpRequest(static function (): void { throw new InvariantViolation("Shouldn't read from php://input for urlencoded request"); }); } /** - * @param mixed[] $getValue + * @param array $getValue * - * @return OperationParams[]|OperationParams + * @return OperationParams|array */ private function parsePsrGetRequest(array $getValue) { - $helper = new Helper(); - - return $helper->parsePsrRequest( + return (new Helper())->parsePsrRequest( new Request('GET', (new Uri())->withQuery(http_build_query($getValue))) ); } @@ -233,39 +226,36 @@ public function testParsesMultipartFormdataRequest(): void ]; foreach ($parsed as $method => $parsedBody) { + self::assertInstanceOf(OperationParams::class, $parsedBody); self::assertValidOperationParams($parsedBody, $query, null, $variables, $operation, null, $method); self::assertFalse($parsedBody->readOnly, $method); } } /** - * @param mixed[] $postValue + * @param array $postValue * - * @return OperationParams|OperationParams[] + * @return OperationParams|array */ - private function parseRawMultipartFormDataRequest($postValue) + private function parseRawMultipartFormDataRequest(array $postValue) { $_SERVER['CONTENT_TYPE'] = 'multipart/form-data; boundary=----FormBoundary'; $_SERVER['REQUEST_METHOD'] = 'POST'; $_POST = $postValue; - $helper = new Helper(); - - return $helper->parseHttpRequest(static function (): void { + return (new Helper())->parseHttpRequest(static function (): void { throw new InvariantViolation("Shouldn't read from php://input for multipart/form-data request"); }); } /** - * @param mixed[] $postValue + * @param array $postValue * - * @return OperationParams|OperationParams[] + * @return OperationParams|array */ - private function parsePsrMultipartFormDataRequest($postValue) + private function parsePsrMultipartFormDataRequest(array $postValue) { - $helper = new Helper(); - - return $helper->parsePsrRequest( + return (new Helper())->parsePsrRequest( new Request( 'POST', '', @@ -291,6 +281,7 @@ public function testParsesJSONRequest(): void 'psr' => $this->parsePsrRequest('application/json', json_encode($body)), ]; foreach ($parsed as $method => $parsedBody) { + self::assertInstanceOf(OperationParams::class, $parsedBody); self::assertValidOperationParams($parsedBody, $query, null, $variables, $operation, null, $method); self::assertFalse($parsedBody->readOnly, $method); } @@ -314,6 +305,7 @@ public function testParsesParamsAsJSON(): void 'psr' => $this->parsePsrRequest('application/json', json_encode($body)), ]; foreach ($parsed as $method => $parsedBody) { + self::assertInstanceOf(OperationParams::class, $parsedBody); self::assertValidOperationParams($parsedBody, $query, null, $variables, $operation, $extensions, $method); self::assertFalse($parsedBody->readOnly, $method); } @@ -335,6 +327,7 @@ public function testIgnoresInvalidVariablesJson(): void 'psr' => $this->parsePsrRequest('application/json', json_encode($body)), ]; foreach ($parsed as $method => $parsedBody) { + self::assertInstanceOf(OperationParams::class, $parsedBody); self::assertValidOperationParams($parsedBody, $query, null, $variables, $operation, null, $method); self::assertFalse($parsedBody->readOnly, $method); } @@ -357,6 +350,7 @@ public function testParsesApolloPersistedQueryJSONRequest(): void 'psr' => $this->parsePsrRequest('application/json', json_encode($body)), ]; foreach ($parsed as $method => $parsedBody) { + self::assertInstanceOf(OperationParams::class, $parsedBody); self::assertValidOperationParams($parsedBody, null, $queryId, $variables, $operation, $extensions, $method); self::assertFalse($parsedBody->readOnly, $method); } diff --git a/tests/Server/ServerConfigTest.php b/tests/Server/ServerConfigTest.php index 4c35d5f10..66ccb4d99 100644 --- a/tests/Server/ServerConfigTest.php +++ b/tests/Server/ServerConfigTest.php @@ -7,6 +7,7 @@ use Closure; use GraphQL\Error\DebugFlag; use GraphQL\Error\InvariantViolation; +use GraphQL\Executor\ExecutionResult; use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter; use GraphQL\Server\ServerConfig; use GraphQL\Type\Definition\ObjectType; @@ -16,6 +17,10 @@ use PHPUnit\Framework\TestCase; use stdClass; +/** + * @phpstan-import-type SerializableError from ExecutionResult + * @phpstan-import-type SerializableErrors from ExecutionResult + */ class ServerConfigTest extends TestCase { public function testDefaults(): void @@ -87,11 +92,11 @@ public function testAllowsSettingErrorFormatter(): void } /** - * @return array + * @return SerializableError */ public static function formatError(): array { - return []; + return ['message' => 'irrelevant']; } public function testAllowsSettingErrorsHandler(): void @@ -108,7 +113,7 @@ public function testAllowsSettingErrorsHandler(): void } /** - * @return array> + * @return SerializableErrors */ public static function handleError(): array { @@ -237,12 +242,14 @@ public function testThrowsOnInvalidArrayKey(): void public function testInvalidValidationRules(): void { - $rules = new stdClass(); $config = ServerConfig::create(); + $rules = new stdClass(); - $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage('Server config expects array of validation rules or callable returning such array, but got instance of stdClass'); + $this->expectExceptionObject(new InvariantViolation( + 'Server config expects array of validation rules or callable returning such array, but got instance of stdClass' + )); + // @phpstan-ignore-next-line intentionally wrong $config->setValidationRules($rules); } } diff --git a/tests/Server/StandardServerTest.php b/tests/Server/StandardServerTest.php index 9c6c1699b..be2934864 100644 --- a/tests/Server/StandardServerTest.php +++ b/tests/Server/StandardServerTest.php @@ -11,9 +11,9 @@ use GraphQL\Server\OperationParams; use GraphQL\Server\ServerConfig; use GraphQL\Server\StandardServer; -use function json_encode; use Nyholm\Psr7\Request; use Psr\Http\Message\RequestInterface; +use function Safe\json_encode; class StandardServerTest extends ServerTestCase { @@ -37,11 +37,14 @@ public function testSimpleRequestExecutionWithOutsideParsing(): void $server = new StandardServer($this->config); $result = $server->executeRequest($parsedBody); - $expected = [ - 'data' => ['f1' => 'f1'], - ]; - self::assertEquals($expected, $result->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE)); + self::assertInstanceOf(ExecutionResult::class, $result); + self::assertSame( + [ + 'data' => ['f1' => 'f1'], + ], + $result->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE) + ); } private function parseRawRequest(string $contentType, string $content, string $method = 'POST'): OperationParams @@ -51,7 +54,10 @@ private function parseRawRequest(string $contentType, string $content, string $m $helper = new Helper(); - return $helper->parseHttpRequest(static fn () => $content); + $operationParams = $helper->parseHttpRequest(static fn () => $content); + self::assertInstanceOf(OperationParams::class, $operationParams); + + return $operationParams; } public function testSimplePsrRequestExecution(): void @@ -89,7 +95,10 @@ private function assertPsrRequestEquals(array $expected, RequestInterface $reque private function executePsrRequest(RequestInterface $psrRequest): ExecutionResult { - return (new StandardServer($this->config))->executePsrRequest($psrRequest); + $result = (new StandardServer($this->config))->executePsrRequest($psrRequest); + self::assertInstanceOf(ExecutionResult::class, $result); + + return $result; } public function testMultipleOperationPsrRequestExecution(): void diff --git a/tests/StarWarsSchema.php b/tests/StarWarsSchema.php index 453332106..d3a1e5728 100644 --- a/tests/StarWarsSchema.php +++ b/tests/StarWarsSchema.php @@ -90,6 +90,7 @@ public static function build(): Schema /** @var ObjectType $humanType */ $humanType = null; + /** @var ObjectType $humanType */ $droidType = null; diff --git a/tests/Type/DefinitionTest.php b/tests/Type/DefinitionTest.php index a69a5fb94..3cd49fdfa 100644 --- a/tests/Type/DefinitionTest.php +++ b/tests/Type/DefinitionTest.php @@ -27,8 +27,8 @@ use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Schema; -use function json_encode; use RuntimeException; +use function Safe\json_encode; use function sprintf; use stdClass; @@ -354,7 +354,6 @@ public function testAcceptsAWellDefinedEnumTypeWithInternalValueDefinition(): vo */ public function testRejectsAnEnumTypeWithIncorrectlyTypedValues(): void { - // @phpstan-ignore-next-line intentionally wrong $enumType = new EnumType([ 'name' => 'SomeEnum', 'values' => [['FOO' => 10]], @@ -960,15 +959,16 @@ public function testAcceptsAnObjectTypeWithInterfacesAsAFunctionReturningAnArray */ public function testRejectsAnObjectTypeWithIncorrectlyTypedInterfaces(): void { + // @phpstan-ignore-next-line intentionally wrong $objType = new ObjectType([ 'name' => 'SomeObject', 'interfaces' => new stdClass(), 'fields' => ['f' => ['type' => Type::string()]], ]); - $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage( + + $this->expectExceptionObject(new InvariantViolation( 'SomeObject interfaces must be an iterable or a callable which returns an iterable.' - ); + )); $objType->assertValid(); } @@ -1108,15 +1108,16 @@ public function testAcceptsAnInterfaceTypeWithInterfacesAsAFunctionReturningAnAr */ public function testRejectsAnInterfaceTypeWithIncorrectlyTypedInterfaces(): void { + // @phpstan-ignore-next-line intentionally wrong $objType = new InterfaceType([ 'name' => 'AnotherInterface', 'interfaces' => new stdClass(), 'fields' => [], ]); - $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage( + + $this->expectExceptionObject(new InvariantViolation( 'AnotherInterface interfaces must be an iterable or a callable which returns an iterable.' - ); + )); $objType->assertValid(); } @@ -1201,16 +1202,17 @@ public function testAcceptsAnInterfaceTypeDefiningResolveTypeWithImplementingTyp */ public function testRejectsAnInterfaceTypeWithAnIncorrectTypeForResolveType(): void { - // Slightly deviating from the reference implementation in order to be idiomatic for PHP - $this->expectExceptionObject(new InvariantViolation( - 'AnotherInterface must provide "resolveType" as a callable, but got: instance of stdClass' - )); - + // @phpstan-ignore-next-line intentionally wrong $type = new InterfaceType([ 'name' => 'AnotherInterface', 'resolveType' => new stdClass(), 'fields' => ['f' => ['type' => Type::string()]], ]); + + // Slightly deviating from the reference implementation in order to be idiomatic for PHP + $this->expectExceptionObject(new InvariantViolation( + 'AnotherInterface must provide "resolveType" as a callable, but got: instance of stdClass' + )); $type->assertValid(); } @@ -1267,7 +1269,9 @@ public function testRejectsAnUnionTypeWithAnIncorrectTypeForResolveType(): void $this->expectExceptionObject(new InvariantViolation( 'SomeUnion must provide "resolveType" as a callable, but got: instance of stdClass' )); + $this->schemaWithFieldType( + // @phpstan-ignore-next-line intentionally wrong new UnionType([ 'name' => 'SomeUnion', 'resolveType' => new stdClass(), @@ -1294,35 +1298,19 @@ public function testAcceptsAScalarTypeDefiningSerialize(): void // Type System: Scalar types must be serializable - /** - * @see it('rejects a Scalar type not defining serialize') - */ - public function testRejectsAScalarTypeNotDefiningSerialize(): void - { - $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage( - 'SomeScalar must provide "serialize" function. If this custom Scalar ' - . 'is also used as an input type, ensure "parseValue" and "parseLiteral" ' - . 'functions are also provided.' - ); - $this->schemaWithFieldType( - // @phpstan-ignore-next-line intentionally wrong - new CustomScalarType(['name' => 'SomeScalar']) - ); - } - /** * @see it('rejects a Scalar type defining serialize with an incorrect type') */ public function testRejectsAScalarTypeDefiningSerializeWithAnIncorrectType(): void { - $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage( + $this->expectExceptionObject(new InvariantViolation( 'SomeScalar must provide "serialize" function. If this custom Scalar ' . 'is also used as an input type, ensure "parseValue" and "parseLiteral" ' . 'functions are also provided.' - ); + )); + $this->schemaWithFieldType( + // @phpstan-ignore-next-line intentionally wrong new CustomScalarType([ 'name' => 'SomeScalar', 'serialize' => new stdClass(), @@ -1354,10 +1342,10 @@ public function testAcceptsAScalarTypeDefiningParseValueAndParseLiteral(): void */ public function testRejectsAScalarTypeDefiningParseValueButNotParseLiteral(): void { - $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage( + $this->expectExceptionObject(new InvariantViolation( 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.' - ); + )); + $this->schemaWithFieldType( new CustomScalarType([ 'name' => 'SomeScalar', @@ -1374,10 +1362,10 @@ public function testRejectsAScalarTypeDefiningParseValueButNotParseLiteral(): vo */ public function testRejectsAScalarTypeDefiningParseLiteralButNotParseValue(): void { - $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage( + $this->expectExceptionObject(new InvariantViolation( 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.' - ); + )); + $this->schemaWithFieldType( new CustomScalarType([ 'name' => 'SomeScalar', @@ -1394,11 +1382,12 @@ public function testRejectsAScalarTypeDefiningParseLiteralButNotParseValue(): vo */ public function testRejectsAScalarTypeDefiningParseValueAndParseLiteralWithAnIncorrectType(): void { - $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage( + $this->expectExceptionObject(new InvariantViolation( 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.' - ); + )); + $this->schemaWithFieldType( + // @phpstan-ignore-next-line intentionally wrong new CustomScalarType([ 'name' => 'SomeScalar', 'serialize' => static function (): void { @@ -1434,7 +1423,9 @@ public function testRejectsAnObjectTypeWithAnIncorrectTypeForIsTypeOf(): void $this->expectExceptionObject(new InvariantViolation( 'AnotherObject must provide "isTypeOf" as a callable, but got: instance of stdClass' )); + $this->schemaWithFieldType( + // @phpstan-ignore-next-line intentionally wrong new ObjectType([ 'name' => 'AnotherObject', 'isTypeOf' => new stdClass(), @@ -1480,10 +1471,10 @@ public function testAcceptsAUnionTypeWithFunctionReturningAnArrayOfTypes(): void */ public function testRejectsAUnionTypeWithoutTypes(): void { - $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage( + $this->expectExceptionObject(new InvariantViolation( 'Must provide iterable of types or a callable which returns such an iterable for Union SomeUnion' - ); + )); + $this->schemaWithFieldType( // @phpstan-ignore-next-line intentionally wrong new UnionType(['name' => 'SomeUnion']) @@ -1495,11 +1486,12 @@ public function testRejectsAUnionTypeWithoutTypes(): void */ public function testRejectsAUnionTypeWithIncorrectlyTypedTypes(): void { - $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage( + $this->expectExceptionObject(new InvariantViolation( 'Must provide iterable of types or a callable which returns such an iterable for Union SomeUnion' - ); + )); + $this->schemaWithFieldType( + // @phpstan-ignore-next-line intentionally wrong new UnionType([ 'name' => 'SomeUnion', 'types' => (object) ['test' => $this->objectType], @@ -1720,6 +1712,8 @@ public function testRejectsAnInputObjectTypeWithResolverConstant(): void $inputObjType->assertValid(); } + // Type System: A Schema must contain uniquely named types + /** * @see it('rejects a Schema which redefines a built-in type') */ @@ -1748,7 +1742,26 @@ public function testRejectsASchemaWhichRedefinesABuiltInType(): void $schema->assertValid(); } - // Type System: A Schema must contain uniquely named types + /** + * @see it('rejects a Schema when a provided type has no name') + */ + public function testRejectsASchemaWhenAProvidedTypeHasNoName(): void + { + self::markTestSkipped('Our types are more strict by default, given we use classes'); + + $QueryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'foo' => ['type' => Type::string()], + ], + ]); + + new Schema( + [ + 'query' => $QueryType, + 'types' => [new stdClass()], ] + ); + } /** * @see it('rejects a Schema which defines an object type twice') @@ -1772,18 +1785,46 @@ public function testRejectsASchemaWhichDefinesAnObjectTypeTwice(): void 'b' => ['type' => $B], ], ]); - $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage( + + $this->expectExceptionObject(new InvariantViolation( 'Schema must contain unique named types but contains multiple types named "SameName" ' . '(see https://webonyx.github.io/graphql-php/type-definitions/#type-registry).' - ); - $schema = new Schema(['query' => $QueryType]); - $schema->assertValid(); + )); + new Schema(['query' => $QueryType]); } /** - * @see it('rejects a Schema which have same named objects implementing an interface') + * @see it('rejects a Schema which defines fields with conflicting types' */ + public function testRejectsASchemaWhichDefinesFieldsWithConflictingTypes(): void + { + $fields = ['f' => ['type' => Type::string()]]; + + $A = new ObjectType([ + 'name' => 'SameName', + 'fields' => $fields, + ]); + + $B = new ObjectType([ + 'name' => 'SameName', + 'fields' => $fields, + ]); + + $QueryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'a' => ['type' => $A], + 'b' => ['type' => $B], + ], + ]); + + $this->expectExceptionObject(new InvariantViolation( + 'Schema must contain unique named types but contains multiple types named "SameName" ' + . '(see https://webonyx.github.io/graphql-php/type-definitions/#type-registry).' + )); + new Schema(['query' => $QueryType]); + } + public function testRejectsASchemaWhichHaveSameNamedObjectsImplementingAnInterface(): void { $AnotherInterface = new InterfaceType([ @@ -1822,11 +1863,6 @@ public function testRejectsASchemaWhichHaveSameNamedObjectsImplementingAnInterfa $schema->assertValid(); } - // Lazy Fields - - /** - * @see it('allows a type to define its fields as closure returning array field definition to be lazy loaded') - */ public function testAllowsTypeWhichDefinesItFieldsAsClosureReturningFieldDefinitionAsArray(): void { $objType = new ObjectType([ @@ -1843,17 +1879,15 @@ public function testAllowsTypeWhichDefinesItFieldsAsClosureReturningFieldDefinit self::assertSame(Type::string(), $objType->getField('f')->getType()); } - /** - * @see it('allows a type to define its fields as closure returning object field definition to be lazy loaded') - */ public function testAllowsTypeWhichDefinesItFieldsAsClosureReturningFieldDefinitionAsObject(): void { $objType = new ObjectType([ 'name' => 'SomeObject', 'fields' => [ - 'f' => static function (): FieldDefinition { - return FieldDefinition::create(['name' => 'f', 'type' => Type::string()]); - }, + 'f' => static fn (): FieldDefinition => new FieldDefinition([ + 'name' => 'f', + 'type' => Type::string(), + ]), ], ]); @@ -1907,9 +1941,6 @@ public function testFieldClosureNotExecutedIfNotAccessed(): void self::assertSame(1, $resolvedCount); } - /** - * @see it('does resolve all field definitions when validating the type') - */ public function testAllUnresolvedFieldsAreResolvedWhenValidatingType(): void { $resolvedCount = 0; @@ -1932,88 +1963,17 @@ public function testAllUnresolvedFieldsAreResolvedWhenValidatingType(): void self::assertSame(2, $resolvedCount); } - /** - * @see it('does throw when lazy loaded array field definition changes its name') - */ - public function testThrowsWhenLazyLoadedArrayFieldDefinitionChangesItsName(): void - { - $objType = new ObjectType([ - 'name' => 'SomeObject', - 'fields' => [ - 'f' => static function (): array { - return ['name' => 'foo', 'type' => Type::string()]; - }, - ], - ]); - - $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage( - 'SomeObject.f should not dynamically change its name when resolved lazily.' - ); - - $objType->assertValid(); - } - - /** - * @see it('does throw when lazy loaded object field definition changes its name') - */ - public function testThrowsWhenLazyLoadedObjectFieldDefinitionChangesItsName(): void - { - $objType = new ObjectType([ - 'name' => 'SomeObject', - 'fields' => [ - 'f' => static function (): FieldDefinition { - return FieldDefinition::create(['name' => 'foo', 'type' => Type::string()]); - }, - ], - ]); - - $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage( - 'SomeObject.f should not dynamically change its name when resolved lazily.' - ); - - $objType->assertValid(); - } - - /** - * @see it('does throw when lazy loaded field definition has no keys for field names') - */ public function testThrowsWhenLazyLoadedFieldDefinitionHasNoKeysForFieldNames(): void { $objType = new ObjectType([ 'name' => 'SomeObject', 'fields' => [ - static function (): array { - return ['type' => Type::string()]; - }, - ], - ]); - - $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage( - 'SomeObject lazy fields must be an associative array with field names as keys.' - ); - - $objType->assertValid(); - } - - /** - * @see it('does throw when lazy loaded field definition has invalid args') - */ - public function testThrowsWhenLazyLoadedFieldHasInvalidArgs(): void - { - $objType = new ObjectType([ - 'name' => 'SomeObject', - 'fields' => [ - 'f' => static function (): array { - return ['args' => 'invalid', 'type' => Type::string()]; - }, + static fn (): array => ['type' => Type::string()], ], ]); $this->expectExceptionObject(new InvariantViolation( - 'SomeObject.f args must be an array.' + 'SomeObject lazy fields must be an associative array with field names as keys.' )); $objType->assertValid(); diff --git a/tests/Type/EnumTypeTest.php b/tests/Type/EnumTypeTest.php index 34a385997..d236b093d 100644 --- a/tests/Type/EnumTypeTest.php +++ b/tests/Type/EnumTypeTest.php @@ -28,7 +28,7 @@ class EnumTypeTest extends TestCase private EnumType $ComplexEnum; /** @var array{someRandomFunction: callable(): void} */ - private $Complex1; + private array $Complex1; /** @var ArrayObject */ private ArrayObject $Complex2; @@ -293,7 +293,7 @@ public function testDoesNotAcceptStringLiterals(): void '{ colorEnum(fromEnum: "GREEN") }', null, [ - 'message' => 'Field "colorEnum" argument "fromEnum" requires type Color, found "GREEN"; Did you mean the enum value GREEN?', + 'message' => 'Enum "Color" cannot represent non-enum value: "GREEN". Did you mean the enum value "GREEN"?', 'locations' => [new SourceLocation(1, 23)], ] ); @@ -326,7 +326,7 @@ private function expectFailure(string $query, ?array $vars, $err): void } /** - * @see it('does not accept valuesNotInTheEnum') + * @see it('does not accept values not in the enum') */ public function testDoesNotAcceptValuesNotInTheEnum(): void { @@ -334,7 +334,7 @@ public function testDoesNotAcceptValuesNotInTheEnum(): void '{ colorEnum(fromEnum: GREENISH) }', null, [ - 'message' => 'Field "colorEnum" argument "fromEnum" requires type Color, found GREENISH; Did you mean the enum value GREEN?', + 'message' => 'Value "GREENISH" does not exist in "Color" enum. Did you mean the enum value "GREEN"?', 'locations' => [new SourceLocation(1, 23)], ] ); @@ -349,7 +349,8 @@ public function testDoesNotAcceptValuesWithIncorrectCasing(): void '{ colorEnum(fromEnum: green) }', null, [ - 'message' => 'Field "colorEnum" argument "fromEnum" requires type Color, found green; Did you mean the enum value GREEN?', + // Improves upon the reference implementation + 'message' => 'Value "green" does not exist in "Color" enum. Did you mean the enum value "GREEN"?', 'locations' => [new SourceLocation(1, 23)], ] ); @@ -379,7 +380,7 @@ public function testDoesNotAcceptInternalValueInPlaceOfEnumLiteral(): void $this->expectFailure( '{ colorEnum(fromEnum: 1) }', null, - 'Field "colorEnum" argument "fromEnum" requires type Color, found 1.' + 'Enum "Color" cannot represent non-enum value: 1.' ); } @@ -391,7 +392,7 @@ public function testDoesNotAcceptEnumLiteralInPlaceOfInt(): void $this->expectFailure( '{ colorEnum(fromInt: GREEN) }', null, - 'Field "colorEnum" argument "fromInt" requires type Int, found GREEN.' + 'Int cannot represent non-integer value: GREEN' ); } diff --git a/tests/Type/IntrospectionTest.php b/tests/Type/IntrospectionTest.php index 5f483fdee..cd9f83368 100644 --- a/tests/Type/IntrospectionTest.php +++ b/tests/Type/IntrospectionTest.php @@ -15,8 +15,8 @@ use GraphQL\Type\Introspection; use GraphQL\Type\Schema; use GraphQL\Validator\Rules\ProvidedRequiredArguments; -use function json_encode; use PHPUnit\Framework\TestCase; +use function Safe\json_encode; use function sprintf; class IntrospectionTest extends TestCase @@ -1000,8 +1000,7 @@ public function testIntrospectsOnInputObject(): void ]; $result = GraphQL::executeQuery($schema, $request)->toArray(); - $result = $result['data']['__type']; - self::assertEquals($expectedFragment, $result); + self::assertEquals($expectedFragment, $result['data']['__type'] ?? null); } /** diff --git a/tests/Type/LazyTypeLoaderTest.php b/tests/Type/LazyTypeLoaderTest.php index e62b68b67..47d091730 100644 --- a/tests/Type/LazyTypeLoaderTest.php +++ b/tests/Type/LazyTypeLoaderTest.php @@ -19,7 +19,7 @@ final class LazyTypeLoaderTest extends TypeLoaderTest /** @var callable(): InterfaceType */ private $node; - /** @var callable(): ObjectType */ + /** @var callable(): InterfaceType */ private $content; /** @var callable(): ObjectType */ diff --git a/tests/Type/QueryPlanTest.php b/tests/Type/QueryPlanTest.php index 8baa92d70..53d160df4 100644 --- a/tests/Type/QueryPlanTest.php +++ b/tests/Type/QueryPlanTest.php @@ -950,9 +950,11 @@ public function testQueryPlanGroupingImplementorFieldsForAbstractTypes(): void 'fields' => [ 'item' => [ 'type' => $item, - 'resolve' => static function ($value, $args, $context, ResolveInfo $info) use (&$hasCalled, &$queryPlan) { + 'resolve' => static function ($value, array $args, $context, ResolveInfo $info) use (&$hasCalled, &$queryPlan) { $hasCalled = true; - $queryPlan = $info->lookAhead(['group-implementor-fields']); + $queryPlan = $info->lookAhead([ + 'groupImplementorFields' => true, + ]); return null; }, diff --git a/tests/Type/ScalarSerializationTest.php b/tests/Type/ScalarSerializationTest.php index dd9d364ca..47973e389 100644 --- a/tests/Type/ScalarSerializationTest.php +++ b/tests/Type/ScalarSerializationTest.php @@ -204,11 +204,11 @@ public function testSerializesOutputAsID(): void public function badIDValues(): iterable { return [ - [new stdClass(), 'ID cannot represent value: instance of stdClass'], - [true, 'ID cannot represent value: true'], - [false, 'ID cannot represent value: false'], - [-1.1, 'ID cannot represent value: -1.1'], - [['abc'], 'ID cannot represent value: ["abc"]'], + [new stdClass(), 'ID cannot represent a non-string and non-integer value: instance of stdClass'], + [true, 'ID cannot represent a non-string and non-integer value: true'], + [false, 'ID cannot represent a non-string and non-integer value: false'], + [-1.1, 'ID cannot represent a non-string and non-integer value: -1.1'], + [['abc'], 'ID cannot represent a non-string and non-integer value: ["abc"]'], ]; } @@ -221,8 +221,9 @@ public function testSerializesOutputAsIDError($value, string $expectedError): vo { $idType = Type::id(); - $this->expectException(SerializationError::class); - $this->expectExceptionMessage($expectedError); + $this->expectExceptionObject(new SerializationError( + $expectedError + )); $idType->serialize($value); } } diff --git a/tests/Type/SchemaTest.php b/tests/Type/SchemaTest.php index 1e2bc9931..f87075324 100644 --- a/tests/Type/SchemaTest.php +++ b/tests/Type/SchemaTest.php @@ -4,16 +4,14 @@ namespace GraphQL\Tests\Type; +use GraphQL\Error\Error; use GraphQL\Error\InvariantViolation; -use GraphQL\Type\Definition\AbstractType; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; -use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; -use InvalidArgumentException; use PHPUnit\Framework\TestCase; class SchemaTest extends TestCase @@ -127,32 +125,4 @@ public function testIncludesInputTypesOnlyUsedInDirectives(): void self::assertArrayHasKey('DirInput', $typeMap); self::assertArrayHasKey('WrappedDirInput', $typeMap); } - - // Sub Type - - /** - * @see it('validates argument to isSubType to be of the correct type') - */ - public function testThrowsInvalidArgumentExceptionWhenInvalidTypeIsPassedToIsSubType(): void - { - $this->expectException(InvalidArgumentException::class); - - $anonymousAbstractType = new class() implements AbstractType { - public function resolveType($objectValue, $context, ResolveInfo $info) - { - return null; - } - }; - - $this->schema->isSubType( - // @phpstan-ignore-next-line purposefully wrong - $anonymousAbstractType, - new InterfaceType([ - 'name' => 'Interface', - 'fields' => [ - 'irrelevant' => Type::int(), - ], - ]) - ); - } } diff --git a/tests/Type/ValidationTest.php b/tests/Type/ValidationTest.php index b016a197b..3bcbcf3b4 100644 --- a/tests/Type/ValidationTest.php +++ b/tests/Type/ValidationTest.php @@ -21,6 +21,7 @@ use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; +use GraphQL\Type\Definition\NullableType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; @@ -153,7 +154,10 @@ private function withModifiers(array $types): array $types ), array_map( - static fn (Type $type): NonNull => Type::nonNull($type), + static function (Type $type): NonNull { + /** @var Type&NullableType $type */ + return Type::nonNull($type); + }, $types ), array_map( @@ -183,7 +187,6 @@ public function testRejectsTypesWithoutNames(): void static fn (): UnionType => new UnionType([]), // @phpstan-ignore-next-line intentionally wrong static fn (): InterfaceType => new InterfaceType([]), - // @phpstan-ignore-next-line intentionally wrong static fn (): ScalarType => new CustomScalarType([]), ], 'Must provide name.' @@ -352,7 +355,7 @@ private function formatLocations(Error $error): array /** * @param array $errors * - * @return array}> + * @return array}> */ private function formatErrors(array $errors, bool $withLocation = true): array { @@ -815,7 +818,11 @@ public function testRejectsAUnionTypeWithNonObjectMembersType(): void foreach ($badUnionMemberTypes as $memberType) { $badSchema = $this->schemaWithFieldType( - new UnionType(['name' => 'BadUnion', 'types' => [$memberType]]) + // @phpstan-ignore-next-line intentionally wrong + new UnionType([ + 'name' => 'BadUnion', + 'types' => [$memberType], + ]) ); $this->assertMatchesValidationMessage( $badSchema->validate(), @@ -1661,10 +1668,13 @@ public function testAcceptsAnInputTypeAsAnInputFieldType(): void private function schemaWithInputFieldOfType(Type $inputFieldType): Schema { + // @phpstan-ignore-next-line intentionally wrong $badInputObjectType = new InputObjectType([ 'name' => 'BadInputObject', 'fields' => [ - 'badField' => ['type' => $inputFieldType], + 'badField' => [ + 'type' => $inputFieldType, + ], ], ]); @@ -2958,6 +2968,7 @@ public function testRejectsASchemaWithDirectivesWithWrongArgs(): void ], ], ]); + // @phpstan-ignore-next-line intentionally wrong $directive = new Directive([ 'name' => 'test', 'args' => [ diff --git a/tests/UserArgsTest.php b/tests/UserArgsTest.php index 4eab1d9cb..05dedc239 100644 --- a/tests/UserArgsTest.php +++ b/tests/UserArgsTest.php @@ -24,7 +24,7 @@ public function testErrorForNonExistentScalarInputField(): void } '; $result = GraphQL::executeQuery($this->schema(), $query)->toArray(); - self::assertEquals('Field "scalar" is not defined by type InputType.', $result['errors'][0]['message']); + self::assertEquals('Field "scalar" is not defined by type "InputType".', $result['errors'][0]['message'] ?? null); } public function testErrorForNonExistentArrayInputField(): void @@ -38,7 +38,7 @@ public function testErrorForNonExistentArrayInputField(): void } '; $result = GraphQL::executeQuery($this->schema(), $query)->toArray(); - self::assertEquals('Field "array" is not defined by type InputType.', $result['errors'][0]['message']); + self::assertEquals('Field "array" is not defined by type "InputType".', $result['errors'][0]['message'] ?? null); } private function schema(): Schema diff --git a/tests/Utils/BuildSchemaTest.php b/tests/Utils/BuildSchemaTest.php index 6c76c029a..60b1e0c38 100644 --- a/tests/Utils/BuildSchemaTest.php +++ b/tests/Utils/BuildSchemaTest.php @@ -47,8 +47,14 @@ public function testUseBuiltSchemaForLimitedExecution(): void } ')); - $result = GraphQL::executeQuery($schema, '{ str }', ['str' => 123]); - self::assertEquals(['str' => 123], $result->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE)['data']); + $data = ['str' => 123]; + + self::assertEquals( + [ + 'data' => $data, + ], + GraphQL::executeQuery($schema, '{ str }', $data)->toArray() + ); } /** diff --git a/tests/Utils/CoerceValueTest.php b/tests/Utils/CoerceValueTest.php index 76ecf8b18..303b29d99 100644 --- a/tests/Utils/CoerceValueTest.php +++ b/tests/Utils/CoerceValueTest.php @@ -71,11 +71,11 @@ public function testCoercingAnArrayToGraphQLIDProducesAnError(): void $result = Value::coerceValue([1, 2, 3], Type::id()); $this->expectGraphQLError( $result, - 'Expected type ID; ID cannot represent value: [1,2,3]' + 'Expected type ID; ID cannot represent a non-string and non-integer value: [1,2,3]' ); self::assertEquals( - 'ID cannot represent value: [1,2,3]', + 'ID cannot represent a non-string and non-integer value: [1,2,3]', $result['errors'][0]->getPrevious()->getMessage() ); } diff --git a/tests/Utils/SchemaExtenderTest.php b/tests/Utils/SchemaExtenderTest.php index af5fa01cf..056dfcf14 100644 --- a/tests/Utils/SchemaExtenderTest.php +++ b/tests/Utils/SchemaExtenderTest.php @@ -14,6 +14,7 @@ use GraphQL\GraphQL; use GraphQL\Language\AST\DefinitionNode; use GraphQL\Language\AST\DocumentNode; +use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeList; use GraphQL\Language\DirectiveLocation; @@ -588,15 +589,17 @@ enum EnumWithDeprecatedValue { } '); - /** @var ObjectType $typeWithDeprecatedField */ $typeWithDeprecatedField = $extendedSchema->getType('TypeWithDeprecatedField'); + self::assertInstanceOf(ObjectType::class, $typeWithDeprecatedField); + $deprecatedFieldDef = $typeWithDeprecatedField->getField('newDeprecatedField'); self::assertEquals(true, $deprecatedFieldDef->isDeprecated()); self::assertEquals('not used anymore', $deprecatedFieldDef->deprecationReason); - /** @var EnumType $enumWithDeprecatedValue */ $enumWithDeprecatedValue = $extendedSchema->getType('EnumWithDeprecatedValue'); + self::assertInstanceOf(EnumType::class, $enumWithDeprecatedValue); + $deprecatedEnumDef = $enumWithDeprecatedValue->getValue('DEPRECATED'); self::assertEquals(true, $deprecatedEnumDef->isDeprecated()); @@ -613,8 +616,10 @@ public function testExtendsObjectsWithDeprecatedFields(): void deprecatedField: String @deprecated(reason: "not used anymore") } '); - /** @var ObjectType $fooType */ + $fooType = $extendedSchema->getType('Foo'); + self::assertInstanceOf(ObjectType::class, $fooType); + $deprecatedFieldDef = $fooType->getField('deprecatedField'); self::assertTrue($deprecatedFieldDef->isDeprecated()); @@ -632,8 +637,9 @@ public function testExtendsEnumsWithDeprecatedValues(): void } '); - /** @var EnumType $someEnumType */ $someEnumType = $extendedSchema->getType('SomeEnum'); + self::assertInstanceOf(EnumType::class, $someEnumType); + $deprecatedEnumDef = $someEnumType->getValue('DEPRECATED'); self::assertTrue($deprecatedEnumDef->isDeprecated()); @@ -1685,7 +1691,7 @@ public function testPreservesScalarClassMethods(): void self::assertInstanceOf(CustomScalarType::class, $extendedScalar); self::assertSame(SomeScalarClassType::SERIALIZE_RETURN, $extendedScalar->serialize(null)); self::assertSame(SomeScalarClassType::PARSE_VALUE_RETURN, $extendedScalar->parseValue(null)); - self::assertSame(SomeScalarClassType::PARSE_LITERAL_RETURN, $extendedScalar->parseLiteral(Parser::valueLiteral('1'))); + self::assertSame(SomeScalarClassType::PARSE_LITERAL_RETURN, $extendedScalar->parseLiteral(new IntValueNode(['value' => '1']))); } public function testPreservesResolveTypeMethod(): void diff --git a/tests/Validator/ValidationTest.php b/tests/Validator/ValidationTest.php index cb7fb3fc7..4a785372b 100644 --- a/tests/Validator/ValidationTest.php +++ b/tests/Validator/ValidationTest.php @@ -27,30 +27,6 @@ public function testValidatesQueries(): void '); } - /** - * @see it('detects bad scalar parse') - */ - public function testDetectsBadScalarParse(): void - { - $doc = ' - query { - invalidArg(arg: "bad value") - } - '; - - $expectedError = [ - 'message' => 'Field "invalidArg" argument "arg" requires type Invalid, found "bad value"; Invalid scalar is always invalid: bad value', - 'locations' => [['line' => 3, 'column' => 25]], - ]; - - $this->expectInvalid( - self::getTestSchema(), - null, - $doc, - [$expectedError] - ); - } - public function testPassesValidationWithEmptyRules(): void { $query = '{invalid}'; diff --git a/tests/Validator/ValidatorTestCase.php b/tests/Validator/ValidatorTestCase.php index 6d14b5f0d..b550b6ff0 100644 --- a/tests/Validator/ValidatorTestCase.php +++ b/tests/Validator/ValidatorTestCase.php @@ -7,6 +7,8 @@ use function array_map; use GraphQL\Error\Error; use GraphQL\Error\FormattedError; +use GraphQL\Error\UserError; +use GraphQL\Language\AST\Node; use GraphQL\Language\DirectiveLocation; use GraphQL\Language\Parser; use GraphQL\Type\Definition\CustomScalarType; @@ -307,11 +309,11 @@ public static function getTestSchema(): Schema 'serialize' => static function ($value) { return $value; }, - 'parseLiteral' => static function ($node): void { - throw new Error('Invalid scalar is always invalid: ' . $node->value); + 'parseLiteral' => static function (Node $node): void { + throw new UserError('Invalid scalar is always invalid: ' . $node->value); }, 'parseValue' => static function ($node): void { - throw new Error('Invalid scalar is always invalid: ' . $node); + throw new UserError('Invalid scalar is always invalid: ' . $node); }, ]); diff --git a/tests/Validator/ValuesOfCorrectTypeTest.php b/tests/Validator/ValuesOfCorrectTypeTest.php index 57c4f19dd..69a36315a 100644 --- a/tests/Validator/ValuesOfCorrectTypeTest.php +++ b/tests/Validator/ValuesOfCorrectTypeTest.php @@ -6,6 +6,10 @@ use GraphQL\Language\SourceLocation; use GraphQL\Tests\ErrorHelper; +use GraphQL\Type\Definition\CustomScalarType; +use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\Type; +use GraphQL\Type\Schema; use GraphQL\Validator\Rules\ValuesOfCorrectType; /** @@ -243,30 +247,13 @@ public function testIntIntoString(): void } ', [ - $this->badValueWithMessage('Field "stringArgField" argument "stringArg" requires type String, found 1.', 4, 39), + $this->badValueWithMessage('String cannot represent a non string value: 1', 4, 39), ] ); self::assertTrue($errors[0]->isClientSafe()); } - /** - * @param mixed $value anything - * - * @phpstan-return ErrorArray - */ - private function badValue(string $typeName, $value, int $line, int $column, ?string $message = null): array - { - return ErrorHelper::create( - ValuesOfCorrectType::badValueMessage( - $typeName, - $value, - $message - ), - [new SourceLocation($line, $column)] - ); - } - /** * @phpstan-return ErrorArray */ @@ -290,7 +277,7 @@ public function testFloatIntoString(): void } ', [ - $this->badValueWithMessage('Field "stringArgField" argument "stringArg" requires type String, found 1.0.', 4, 39), + $this->badValueWithMessage('String cannot represent a non string value: 1.0', 4, 39), ] ); @@ -314,7 +301,7 @@ public function testBooleanIntoString(): void } ', [ - $this->badValueWithMessage('Field "stringArgField" argument "stringArg" requires type String, found true.', 4, 39), + $this->badValueWithMessage('String cannot represent a non string value: true', 4, 39), ] ); @@ -336,7 +323,7 @@ public function testUnquotedStringIntoString(): void } ', [ - $this->badValueWithMessage('Field "stringArgField" argument "stringArg" requires type String, found BAR.', 4, 39), + $this->badValueWithMessage('String cannot represent a non string value: BAR', 4, 39), ] ); @@ -358,7 +345,7 @@ public function testStringIntoInt(): void } ', [ - $this->badValueWithMessage('Field "intArgField" argument "intArg" requires type Int, found "3".', 4, 33), + $this->badValueWithMessage('Int cannot represent non-integer value: "3"', 4, 33), ] ); @@ -380,7 +367,7 @@ public function testBigIntIntoInt(): void } ', [ - $this->badValueWithMessage('Field "intArgField" argument "intArg" requires type Int, found 829384293849283498239482938.', 4, 33), + $this->badValueWithMessage('Int cannot represent non-integer value: 829384293849283498239482938', 4, 33), ] ); @@ -404,7 +391,7 @@ public function testUnquotedStringIntoInt(): void } ', [ - $this->badValueWithMessage('Field "intArgField" argument "intArg" requires type Int, found FOO.', 4, 33), + $this->badValueWithMessage('Int cannot represent non-integer value: FOO', 4, 33), ] ); @@ -426,7 +413,7 @@ public function testSimpleFloatIntoInt(): void } ', [ - $this->badValueWithMessage('Field "intArgField" argument "intArg" requires type Int, found 3.0.', 4, 33), + $this->badValueWithMessage('Int cannot represent non-integer value: 3.0', 4, 33), ] ); @@ -448,7 +435,7 @@ public function testFloatIntoInt(): void } ', [ - $this->badValueWithMessage('Field "intArgField" argument "intArg" requires type Int, found 3.333.', 4, 33), + $this->badValueWithMessage('Int cannot represent non-integer value: 3.333', 4, 33), ] ); @@ -470,7 +457,7 @@ public function testStringIntoFloat(): void } ', [ - $this->badValueWithMessage('Field "floatArgField" argument "floatArg" requires type Float, found "3.333".', 4, 37), + $this->badValueWithMessage('Float cannot represent non numeric value: "3.333"', 4, 37), ] ); @@ -492,7 +479,7 @@ public function testBooleanIntoFloat(): void } ', [ - $this->badValueWithMessage('Field "floatArgField" argument "floatArg" requires type Float, found true.', 4, 37), + $this->badValueWithMessage('Float cannot represent non numeric value: true', 4, 37), ] ); @@ -516,7 +503,7 @@ public function testUnquotedIntoFloat(): void } ', [ - $this->badValueWithMessage('Field "floatArgField" argument "floatArg" requires type Float, found FOO.', 4, 37), + $this->badValueWithMessage('Float cannot represent non numeric value: FOO', 4, 37), ] ); @@ -538,7 +525,7 @@ public function testIntIntoBoolean(): void } ', [ - $this->badValueWithMessage('Field "booleanArgField" argument "booleanArg" requires type Boolean, found 2.', 4, 41), + $this->badValueWithMessage('Boolean cannot represent a non boolean value: 2', 4, 41), ] ); @@ -560,7 +547,7 @@ public function testFloatIntoBoolean(): void } ', [ - $this->badValueWithMessage('Field "booleanArgField" argument "booleanArg" requires type Boolean, found 1.0.', 4, 41), + $this->badValueWithMessage('Boolean cannot represent a non boolean value: 1.0', 4, 41), ] ); @@ -584,7 +571,7 @@ public function testStringIntoBoolean(): void } ', [ - $this->badValueWithMessage('Field "booleanArgField" argument "booleanArg" requires type Boolean, found "true".', 4, 41), + $this->badValueWithMessage('Boolean cannot represent a non boolean value: "true"', 4, 41), ] ); @@ -606,7 +593,7 @@ public function testUnquotedIntoBoolean(): void } ', [ - $this->badValueWithMessage('Field "booleanArgField" argument "booleanArg" requires type Boolean, found TRUE.', 4, 41), + $this->badValueWithMessage('Boolean cannot represent a non boolean value: TRUE', 4, 41), ] ); @@ -628,7 +615,7 @@ public function testFloatIntoID(): void } ', [ - $this->badValueWithMessage('Field "idArgField" argument "idArg" requires type ID, found 1.0.', 4, 31), + $this->badValueWithMessage('ID cannot represent a non-string and non-integer value: 1.0', 4, 31), ] ); @@ -650,7 +637,7 @@ public function testBooleanIntoID(): void } ', [ - $this->badValueWithMessage('Field "idArgField" argument "idArg" requires type ID, found true.', 4, 31), + $this->badValueWithMessage('ID cannot represent a non-string and non-integer value: true', 4, 31), ] ); @@ -674,7 +661,7 @@ public function testUnquotedIntoID(): void } ', [ - $this->badValueWithMessage('Field "idArgField" argument "idArg" requires type ID, found SOMETHING.', 4, 31), + $this->badValueWithMessage('ID cannot represent a non-string and non-integer value: SOMETHING', 4, 31), ] ); @@ -696,7 +683,7 @@ public function testIntIntoEnum(): void } ', [ - $this->badValueWithMessage('Field "doesKnowCommand" argument "dogCommand" requires type DogCommand, found 2.', 4, 41), + $this->badValueWithMessage('Enum "DogCommand" cannot represent non-enum value: 2.', 4, 41), ] ); @@ -718,7 +705,7 @@ public function testFloatIntoEnum(): void } ', [ - $this->badValueWithMessage('Field "doesKnowCommand" argument "dogCommand" requires type DogCommand, found 1.0.', 4, 41), + $this->badValueWithMessage('Enum "DogCommand" cannot represent non-enum value: 1.0.', 4, 41), ] ); @@ -742,7 +729,7 @@ public function testStringIntoEnum(): void } ', [ - $this->badValueWithMessage('Field "doesKnowCommand" argument "dogCommand" requires type DogCommand, found "SIT"; Did you mean the enum value SIT?', 4, 41), + $this->badValueWithMessage('Enum "DogCommand" cannot represent non-enum value: "SIT". Did you mean the enum value "SIT"?', 4, 41), ] ); @@ -764,7 +751,7 @@ public function testBooleanIntoEnum(): void } ', [ - $this->badValueWithMessage('Field "doesKnowCommand" argument "dogCommand" requires type DogCommand, found true.', 4, 41), + $this->badValueWithMessage('Enum "DogCommand" cannot represent non-enum value: true.', 4, 41), ] ); @@ -786,7 +773,7 @@ public function testUnknownEnumValueIntoEnum(): void } ', [ - $this->badValueWithMessage('Field "doesKnowCommand" argument "dogCommand" requires type DogCommand, found JUGGLE.', 4, 41), + $this->badValueWithMessage('Value "JUGGLE" does not exist in "DogCommand" enum.', 4, 41), ] ); @@ -808,7 +795,7 @@ public function testDifferentCaseEnumValueIntoEnum(): void } ', [ - $this->badValueWithMessage('Field "doesKnowCommand" argument "dogCommand" requires type DogCommand, found sit; Did you mean the enum value SIT?', 4, 41), + $this->badValueWithMessage('Value "sit" does not exist in "DogCommand" enum. Did you mean the enum value "SIT"?', 4, 41), ] ); @@ -900,7 +887,7 @@ public function testIncorrectItemtype(): void } ', [ - $this->badValueWithMessage('Field "stringListArgField" argument "stringListArg" requires type String, found 2.', 4, 55), + $this->badValueWithMessage('String cannot represent a non string value: 2', 4, 55), ] ); @@ -922,7 +909,7 @@ public function testSingleValueOfIncorrectType(): void } ', [ - $this->badValueWithMessage('Field "stringListArgField" argument "stringListArg" requires type [String], found 1.', 4, 47), + $this->badValueWithMessage('String cannot represent a non string value: 1', 4, 47), ] ); @@ -1118,8 +1105,8 @@ public function testIncorrectValueType(): void } ', [ - $this->badValueWithMessage('Field "multipleReqs" argument "req2" requires type Int!, found "two".', 4, 32), - $this->badValueWithMessage('Field "multipleReqs" argument "req1" requires type Int!, found "one".', 4, 45), + $this->badValueWithMessage('Int cannot represent non-integer value: "two"', 4, 32), + $this->badValueWithMessage('Int cannot represent non-integer value: "one"', 4, 45), ] ); @@ -1128,9 +1115,9 @@ public function testIncorrectValueType(): void } /** - * @see it('Incorrect value and missing argument (ProvidedRequiredArguments)') + * @see it('Incorrect value and missing argument (ProvidedRequiredArgumentsRule)') */ - public function testIncorrectValueAndMissingArgumentProvidedRequiredArguments(): void + public function testIncorrectValueAndMissingArgumentProvidedRequiredArgumentsRule(): void { $errors = $this->expectFailsRule( new ValuesOfCorrectType(), @@ -1142,7 +1129,7 @@ public function testIncorrectValueAndMissingArgumentProvidedRequiredArguments(): } ', [ - $this->badValueWithMessage('Field "multipleReqs" argument "req1" requires type Int!, found "one".', 4, 32), + $this->badValueWithMessage('Int cannot represent non-integer value: "one"', 4, 32), ] ); @@ -1166,7 +1153,7 @@ public function testNullValue2(): void } ', [ - $this->badValueWithMessage('Field "multipleReqs" argument "req1" requires type Int!, found null.', 4, 32), + $this->badValueWithMessage('Expected value of type "Int!", found null.', 4, 32), ] ); @@ -1304,28 +1291,16 @@ public function testPartialObjectMissingRequired(): void } ', [ - $this->requiredField('ComplexInput', 'requiredField', 'Boolean!', 4, 41), + [ + 'message' => 'Field ComplexInput.requiredField of required type Boolean! was not provided.', + 'locations' => [['line' => 4, 'column' => 41]], + ], ] ); self::assertTrue($errors[0]->isClientSafe()); } - /** - * @phpstan-return ErrorArray - */ - private function requiredField(string $typeName, string $fieldName, string $fieldTypeName, int $line, int $column): array - { - return ErrorHelper::create( - ValuesOfCorrectType::requiredFieldMessage( - $typeName, - $fieldName, - $fieldTypeName - ), - [new SourceLocation($line, $column)] - ); - } - // DESCRIBE: Invalid input object value /** @@ -1346,7 +1321,7 @@ public function testPartialObjectInvalidFieldType(): void } ', [ - $this->badValueWithMessage('Field "complexArgField" argument "complexArg" requires type String, found 2.', 5, 40), + $this->badValueWithMessage('String cannot represent a non string value: 2', 5, 40), ] ); @@ -1370,7 +1345,7 @@ public function testPartialObjectNullToNonNullField(): void } } ', - [$this->badValueWithMessage('Field "complexArgField" argument "complexArg" requires type Boolean!, found null.', 6, 29)] + [$this->badValueWithMessage('Expected value of type "Boolean!", found null.', 6, 29)] ); self::assertTrue($errors[0]->isClientSafe()); @@ -1378,10 +1353,6 @@ public function testPartialObjectNullToNonNullField(): void /** * @see it('Partial object, unknown field arg') - * - * The sorting of equal elements has changed so that the test fails on php < 7 - * - * @requires PHP 7.0 */ public function testPartialObjectUnknownFieldArg(): void { @@ -1392,45 +1363,28 @@ public function testPartialObjectUnknownFieldArg(): void complicatedArgs { complexArgField(complexArg: { requiredField: true, - unknownField: "value" + invalidField: "value" }) } } ', [ - $this->unknownField( - 'ComplexInput', - 'unknownField', - 6, - 15 - ), + [ + 'message' => 'Field "invalidField" is not defined by type "ComplexInput". Did you mean "intField"?', + 'locations' => [['line' => 6, 'column' => 15]], + ], ] ); self::assertTrue($errors[0]->isClientSafe()); } - /** - * @phpstan-return ErrorArray - */ - private function unknownField(string $typeName, string $fieldName, int $line, int $column, ?string $message = null): array - { - return ErrorHelper::create( - ValuesOfCorrectType::unknownFieldMessage( - $typeName, - $fieldName, - $message - ), - [new SourceLocation($line, $column)] - ); - } - /** * @see it('reports original error for custom scalar which throws') */ public function testReportsOriginalErrorForCustomScalarWhichThrows(): void { - $errors = $this->expectFailsRule( + $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -1438,7 +1392,7 @@ public function testReportsOriginalErrorForCustomScalarWhichThrows(): void } ', [ - $this->badValueWithMessage('Field "invalidArg" argument "arg" requires type Invalid, found 123; Invalid scalar is always invalid: 123', 3, 27), + $this->badValueWithMessage('Expected value of type "Invalid", found 123; Invalid scalar is always invalid: 123', 3, 27), ] ); } @@ -1448,7 +1402,23 @@ public function testReportsOriginalErrorForCustomScalarWhichThrows(): void */ public function testAllowsCustomScalarToAcceptComplexLiterals(): void { - $this->expectPassesRule( + $customScalar = new CustomScalarType(['name' => 'Any']); + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'anyArg' => [ + 'type' => Type::string(), + 'args' => [ + 'arg' => $customScalar, + ], + ], + ], + ]), + ]); + + $this->expectPassesRuleWithSchema( + $schema, new ValuesOfCorrectType(), ' { @@ -1498,8 +1468,8 @@ public function testWithDirectiveWithIncorrectTypes(): void } ', [ - $this->badValueWithMessage('Field "dog" argument "if" requires type Boolean!, found "yes".', 3, 28), - $this->badValueWithMessage('Field "name" argument "if" requires type Boolean!, found ENUM.', 4, 28), + $this->badValueWithMessage('Boolean cannot represent a non boolean value: "yes"', 3, 28), + $this->badValueWithMessage('Boolean cannot represent a non boolean value: ENUM', 4, 28), ] ); @@ -1565,9 +1535,18 @@ public function testVariablesWithInvalidDefaultNullValues(): void } ', [ - $this->badValue('Int!', 'null', 3, 22), - $this->badValue('String!', 'null', 4, 25), - $this->badValue('Boolean!', 'null', 5, 47), + [ + 'message' => 'Expected value of type "Int!", found null.', + 'locations' => [['line' => 3, 'column' => 22]], + ], + [ + 'message' => 'Expected value of type "String!", found null.', + 'locations' => [['line' => 4, 'column' => 25]], + ], + [ + 'message' => 'Expected value of type "Boolean!", found null.', + 'locations' => [['line' => 5, 'column' => 47]], + ], ] ); @@ -1593,9 +1572,18 @@ public function testVariablesWithInvalidDefaultValues(): void } ', [ - $this->badValue('Int', '"one"', 3, 21), - $this->badValue('String', '4', 4, 24), - $this->badValue('ComplexInput', '"notverycomplex"', 5, 30), + [ + 'message' => 'Int cannot represent non-integer value: "one"', + 'locations' => [['line' => 3, 'column' => 21]], + ], + [ + 'message' => 'String cannot represent a non string value: 4', + 'locations' => [['line' => 4, 'column' => 24]], + ], + [ + 'message' => 'Expected value of type "ComplexInput", found "notverycomplex".', + 'locations' => [['line' => 5, 'column' => 30]], + ], ] ); @@ -1617,8 +1605,14 @@ public function testVariablesWithComplexInvalidDefaultValues(): void } ', [ - $this->badValue('Boolean!', '123', 3, 47), - $this->badValue('Int', '"abc"', 3, 62), + [ + 'message' => 'Boolean cannot represent a non boolean value: 123', + 'locations' => [['line' => 3, 'column' => 47]], + ], + [ + 'message' => 'Int cannot represent non-integer value: "abc"', + 'locations' => [['line' => 3, 'column' => 62]], + ], ] ); @@ -1638,7 +1632,10 @@ public function testComplexVariablesMissingRequiredField(): void } ', [ - $this->requiredField('ComplexInput', 'requiredField', 'Boolean!', 2, 55), + ErrorHelper::create( + 'Field ComplexInput.requiredField of required type Boolean! was not provided.', + [new SourceLocation(2, 55)] + ), ] ); @@ -1658,7 +1655,10 @@ public function testListVariablesWithInvalidItem(): void } ', [ - $this->badValue('String', '2', 2, 50), + [ + 'message' => 'String cannot represent a non string value: 2', + 'locations' => [['line' => 2, 'column' => 50]], + ], ] );