From 300274eee99bf9142fefebac32bfa334521ed1e0 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Mon, 16 Mar 2026 13:21:18 +0100 Subject: [PATCH 1/4] Fix variable validation and input coercion for per-schema scalar overrides Per-schema scalar overrides (#1869) allow type loaders to return custom instances for built-in scalars (ID, String, Int, Float, Boolean). This introduced two bugs where the custom instance and the built-in singleton diverge, causing identity checks to fail. Bug 1: Variable validation rejects valid queries. TypeComparators uses === to compare types. When a variable type is resolved via Schema::getType() (returning the custom instance) but the argument type references the built-in singleton, the identity check fails and VariablesInAllowedPosition reports a spurious error: "Variable "$id" of type "ID!" used in position expecting type "ID!"." Fix: Add name-based equality in isEqualType() and isTypeSubTypeOf() for built-in scalars, so two ScalarType instances with the same built-in name are considered equal. Bug 2: Input coercion calls the wrong parseValue(). Value::coerceInputValue() calls parseValue() on the type from field definitions (the built-in singleton), not the custom override registered via the type loader. Custom parsing logic (e.g. decoding a global ID) is silently skipped. Fix: Pass the Schema into coerceInputValue() and resolve built-in scalars from the schema before calling parseValue(), mirroring the output-side fix already in ReferenceExecutor::completeValue(). Additionally: - Add Type::isBuiltInScalar() to centralize the instanceof + name check used across TypeComparators, Value, and ReferenceExecutor. - Scope the ReferenceExecutor schema type resolution (from #1869) to built-in scalars only, matching the narrower fix elsewhere. - Replace silent fallbacks with assertions when the schema returns an unexpected type for a built-in scalar name. Fixes #1874 Ref #1869 --- src/Executor/ReferenceExecutor.php | 7 +- src/Executor/Values.php | 2 +- src/Type/Definition/Type.php | 15 +++ src/Utils/TypeComparators.php | 18 ++++ src/Utils/Value.php | 23 +++-- tests/Type/ScalarOverridesTest.php | 137 ++++++++++++++++++++++++++++ tests/Utils/TypeComparatorsTest.php | 76 +++++++++++++++ 7 files changed, 267 insertions(+), 11 deletions(-) create mode 100644 tests/Utils/TypeComparatorsTest.php diff --git a/src/Executor/ReferenceExecutor.php b/src/Executor/ReferenceExecutor.php index ecd3514f5..55a865e99 100644 --- a/src/Executor/ReferenceExecutor.php +++ b/src/Executor/ReferenceExecutor.php @@ -912,13 +912,14 @@ protected function completeValue( // instance than `resolveType` or $field->getType() or $arg->getType() assert( $returnType === $this->exeContext->schema->getType($returnType->name) - || in_array($returnType->name, Type::BUILT_IN_SCALAR_NAMES, true), + || Type::isBuiltInScalar($returnType), SchemaValidationContext::duplicateType($this->exeContext->schema, "{$info->parentType}.{$info->fieldName}", $returnType->name) ); if ($returnType instanceof LeafType) { - $schemaType = $this->exeContext->schema->getType($returnType->name); - if ($schemaType instanceof LeafType) { + if (Type::isBuiltInScalar($returnType)) { + $schemaType = $this->exeContext->schema->getType($returnType->name); + assert($schemaType instanceof LeafType, "Schema must provide a LeafType for built-in scalar \"{$returnType->name}\"."); $returnType = $schemaType; } diff --git a/src/Executor/Values.php b/src/Executor/Values.php index 862049fbd..e63737e79 100644 --- a/src/Executor/Values.php +++ b/src/Executor/Values.php @@ -104,7 +104,7 @@ public static function getVariableValues(Schema $schema, NodeList $varDefNodes, } else { // Otherwise, a non-null value was provided, coerce it to the expected // type or report an error if coercion fails. - $coerced = Value::coerceInputValue($value, $varType); + $coerced = Value::coerceInputValue($value, $varType, null, $schema); $coercionErrors = $coerced['errors']; if ($coercionErrors !== null) { diff --git a/src/Type/Definition/Type.php b/src/Type/Definition/Type.php index 9fff72751..68aed12a9 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -213,6 +213,21 @@ public static function overrideStandardTypes(array $types): void } } + /** + * Determines if the given type is a built-in scalar (Int, Float, String, Boolean, ID). + * + * @param mixed $type + * + * @phpstan-assert-if-true ScalarType $type + * + * @api + */ + public static function isBuiltInScalar($type): bool + { + return $type instanceof ScalarType + && in_array($type->name, self::BUILT_IN_SCALAR_NAMES, true); + } + /** * Determines if the given type is an input type. * diff --git a/src/Utils/TypeComparators.php b/src/Utils/TypeComparators.php index 716de3189..cb1a455e1 100644 --- a/src/Utils/TypeComparators.php +++ b/src/Utils/TypeComparators.php @@ -19,6 +19,15 @@ public static function isEqualType(Type $typeA, Type $typeB): bool return true; } + // Built-in scalars may exist as different instances when a type loader + // overrides them. Compare by name to handle this case. + if (Type::isBuiltInScalar($typeA) + && Type::isBuiltInScalar($typeB) + && $typeA->name() === $typeB->name() + ) { + return true; + } + // If either type is non-null, the other must also be non-null. if ($typeA instanceof NonNull && $typeB instanceof NonNull) { return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType()); @@ -46,6 +55,15 @@ public static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type return true; } + // Built-in scalars may exist as different instances when a type loader + // overrides them. Compare by name to handle this case. + if (Type::isBuiltInScalar($maybeSubType) + && Type::isBuiltInScalar($superType) + && $maybeSubType->name() === $superType->name() + ) { + return true; + } + // If superType is non-null, maybeSubType must also be nullable. if ($superType instanceof NonNull) { if ($maybeSubType instanceof NonNull) { diff --git a/src/Utils/Value.php b/src/Utils/Value.php index a39cecb3e..319594a6e 100644 --- a/src/Utils/Value.php +++ b/src/Utils/Value.php @@ -13,6 +13,7 @@ use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; +use GraphQL\Type\Schema; /** * @phpstan-type CoercedValue array{errors: null, value: mixed} @@ -37,7 +38,7 @@ class Value * * @phpstan-return CoercedValue|CoercedErrors */ - public static function coerceInputValue($value, InputType $type, ?array $path = null): array + public static function coerceInputValue($value, InputType $type, ?array $path = null, ?Schema $schema = null): array { if ($type instanceof NonNull) { if ($value === null) { @@ -47,7 +48,7 @@ public static function coerceInputValue($value, InputType $type, ?array $path = } // @phpstan-ignore-next-line wrapped type is known to be input type after schema validation - return self::coerceInputValue($value, $type->getWrappedType(), $path); + return self::coerceInputValue($value, $type->getWrappedType(), $path, $schema); } if ($value === null) { @@ -55,10 +56,16 @@ public static function coerceInputValue($value, InputType $type, ?array $path = return self::ofValue(null); } + // Account for type loader returning a different scalar instance than + // the built-in singleton used in field definitions. Resolve the actual + // type from the schema to ensure the correct parseValue() is called. + if ($schema !== null && Type::isBuiltInScalar($type)) { + $schemaType = $schema->getType($type->name); + assert($schemaType instanceof ScalarType, "Schema must provide a ScalarType for built-in scalar \"{$type->name}\"."); + $type = $schemaType; + } + if ($type instanceof ScalarType || $type instanceof EnumType) { - // Scalars and Enums determine if a input value is valid via parseValue(), which can - // throw to indicate failure. If it throws, maintain a reference to - // the original error. try { return self::ofValue($type->parseValue($value)); } catch (\Throwable $error) { @@ -88,7 +95,8 @@ public static function coerceInputValue($value, InputType $type, ?array $path = $coercedItem = self::coerceInputValue( $itemValue, $itemType, - [...$path ?? [], $index] + [...$path ?? [], $index], + $schema, ); if (isset($coercedItem['errors'])) { @@ -104,7 +112,7 @@ public static function coerceInputValue($value, InputType $type, ?array $path = } // Lists accept a non-list value as a list of one. - $coercedItem = self::coerceInputValue($value, $itemType); + $coercedItem = self::coerceInputValue($value, $itemType, null, $schema); return isset($coercedItem['errors']) ? $coercedItem @@ -133,6 +141,7 @@ public static function coerceInputValue($value, InputType $type, ?array $path = $fieldValue, $field->getType(), [...$path ?? [], $fieldName], + $schema, ); if (isset($coercedField['errors'])) { diff --git a/tests/Type/ScalarOverridesTest.php b/tests/Type/ScalarOverridesTest.php index d93f48000..ebee4ca88 100644 --- a/tests/Type/ScalarOverridesTest.php +++ b/tests/Type/ScalarOverridesTest.php @@ -4,7 +4,9 @@ use GraphQL\Error\InvariantViolation; use GraphQL\GraphQL; +use GraphQL\Language\AST\StringValueNode; use GraphQL\Type\Definition\CustomScalarType; +use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; @@ -204,12 +206,147 @@ public function testNonOverriddenScalarsAreUnaffected(): void self::assertSame('abc-123', $data['identifier']); } + public function testTypeLoaderOverrideWithVariableOfOverriddenBuiltInScalarType(): void + { + $customID = self::createCustomID(static fn ($value): string => (string) $value); + + $queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'node' => [ + 'type' => Type::string(), + 'args' => [ + 'id' => Type::nonNull(Type::id()), + ], + 'resolve' => static fn ($root, array $args): string => 'node-' . $args['id'], + ], + ], + ]); + + $types = ['Query' => $queryType, 'ID' => $customID]; + + $schema = new Schema([ + 'query' => $queryType, + 'typeLoader' => static fn (string $name): ?Type => $types[$name] ?? null, + ]); + + $schema->assertValid(); + + $result = GraphQL::executeQuery($schema, 'query ($id: ID!) { node(id: $id) }', null, null, ['id' => 'abc-123']); + + self::assertEmpty($result->errors, isset($result->errors[0]) ? $result->errors[0]->getMessage() : ''); + self::assertSame(['data' => ['node' => 'node-abc-123']], $result->toArray()); + } + + public function testTypeLoaderOverrideWithNullableVariableOfOverriddenBuiltInScalarType(): void + { + $customString = self::createUppercaseString(); + + $queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'echo' => [ + 'type' => Type::string(), + 'args' => [ + 'text' => Type::string(), + ], + 'resolve' => static fn ($root, array $args): ?string => $args['text'] ?? null, + ], + ], + ]); + + $types = ['Query' => $queryType, 'String' => $customString]; + + $schema = new Schema([ + 'query' => $queryType, + 'typeLoader' => static fn (string $name): ?Type => $types[$name] ?? null, + ]); + + $schema->assertValid(); + + $result = GraphQL::executeQuery($schema, 'query ($text: String) { echo(text: $text) }', null, null, ['text' => 'hello']); + + self::assertEmpty($result->errors, isset($result->errors[0]) ? $result->errors[0]->getMessage() : ''); + self::assertSame(['data' => ['echo' => 'HELLO']], $result->toArray()); + } + + public function testTypeLoaderOverrideWithInputObjectFieldOfOverriddenBuiltInScalarType(): void + { + $customID = self::createCustomID(static fn ($value): string => 'custom-' . $value); + + $inputType = new InputObjectType([ + 'name' => 'NodeInput', + 'fields' => [ + 'id' => Type::nonNull(Type::id()), + 'label' => Type::string(), + ], + ]); + + $queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'node' => [ + 'type' => Type::string(), + 'args' => [ + 'input' => Type::nonNull($inputType), + ], + 'resolve' => static fn ($root, array $args): string => $args['input']['id'] . ':' . ($args['input']['label'] ?? ''), + ], + ], + ]); + + $types = ['Query' => $queryType, 'ID' => $customID, 'NodeInput' => $inputType]; + + $schema = new Schema([ + 'query' => $queryType, + 'typeLoader' => static fn (string $name): ?Type => $types[$name] ?? null, + ]); + + $schema->assertValid(); + + $result = GraphQL::executeQuery( + $schema, + 'query ($input: NodeInput!) { node(input: $input) }', + null, + null, + ['input' => ['id' => 'abc-123', 'label' => 'test']], + ); + + self::assertEmpty($result->errors, isset($result->errors[0]) ? $result->errors[0]->getMessage() : ''); + self::assertSame(['data' => ['node' => 'custom-abc-123:test']], $result->toArray()); + } + + /** @throws InvariantViolation */ + private static function createCustomID(\Closure $parseValue): CustomScalarType + { + return new CustomScalarType([ + 'name' => Type::ID, + 'serialize' => static fn ($value): string => (string) $value, + 'parseValue' => $parseValue, + 'parseLiteral' => static function ($node): string { + if (! $node instanceof StringValueNode) { + throw new \Exception('Expected a string literal for ID.'); + } + + return $node->value; + }, + ]); + } + /** @throws InvariantViolation */ private static function createUppercaseString(): CustomScalarType { return new CustomScalarType([ 'name' => Type::STRING, 'serialize' => static fn ($value): string => strtoupper((string) $value), + 'parseValue' => static fn ($value): string => (string) $value, + 'parseLiteral' => static function ($node): string { + if (! $node instanceof StringValueNode) { + throw new \Exception('Expected a string literal for String.'); + } + + return $node->value; + }, ]); } diff --git a/tests/Utils/TypeComparatorsTest.php b/tests/Utils/TypeComparatorsTest.php new file mode 100644 index 000000000..b7afb05cf --- /dev/null +++ b/tests/Utils/TypeComparatorsTest.php @@ -0,0 +1,76 @@ + Type::STRING]); + + self::assertTrue(TypeComparators::isEqualType(Type::string(), $customString)); + self::assertTrue(TypeComparators::isEqualType($customString, Type::string())); + } + + public function testIsEqualTypeWithWrappedDifferentInstances(): void + { + $customString = new CustomScalarType(['name' => Type::STRING]); + + self::assertTrue(TypeComparators::isEqualType(Type::nonNull(Type::string()), Type::nonNull($customString))); + self::assertTrue(TypeComparators::isEqualType(Type::listOf(Type::string()), Type::listOf($customString))); + self::assertTrue(TypeComparators::isEqualType( + Type::nonNull(Type::listOf(Type::string())), + Type::nonNull(Type::listOf($customString)), + )); + } + + public function testIsTypeSubTypeOfWithDifferentInstancesOfSameNamedType(): void + { + $schema = $this->createSchemaWithCustomString(); + $customString = new CustomScalarType(['name' => Type::STRING]); + + self::assertTrue(TypeComparators::isTypeSubTypeOf($schema, $customString, Type::string())); + self::assertTrue(TypeComparators::isTypeSubTypeOf($schema, Type::string(), $customString)); + } + + public function testIsTypeSubTypeOfWithWrappedDifferentInstances(): void + { + $schema = $this->createSchemaWithCustomString(); + $customString = new CustomScalarType(['name' => Type::STRING]); + + self::assertTrue(TypeComparators::isTypeSubTypeOf($schema, Type::nonNull($customString), Type::string())); + self::assertTrue(TypeComparators::isTypeSubTypeOf($schema, Type::nonNull($customString), Type::nonNull(Type::string()))); + self::assertTrue(TypeComparators::isTypeSubTypeOf( + $schema, + Type::nonNull(Type::listOf(Type::nonNull($customString))), + Type::listOf(Type::nonNull(Type::string())), + )); + } + + /** @throws InvariantViolation */ + private function createSchemaWithCustomString(): Schema + { + $queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'greeting' => [ + 'type' => Type::string(), + 'resolve' => static fn (): string => 'hello', + ], + ], + ]); + + return new Schema([ + 'query' => $queryType, + 'typeLoader' => static fn (string $name): ?Type => ['Query' => $queryType][$name] ?? null, + ]); + } +} From 3d0759dbb9b8c3b9330c683887bee51e910c00fe Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:24:22 +0000 Subject: [PATCH 2/4] Autofix --- docs/class-reference.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/class-reference.md b/docs/class-reference.md index f6e627c43..5a0bc9004 100644 --- a/docs/class-reference.md +++ b/docs/class-reference.md @@ -322,6 +322,19 @@ static function builtInTypes(): array static function builtInScalars(): array ``` +```php +/** + * Determines if the given type is a built-in scalar (Int, Float, String, Boolean, ID). + * + * @param mixed $type + * + * @phpstan-assert-if-true ScalarType $type + * + * @api + */ +static function isBuiltInScalar($type): bool +``` + ```php /** * Determines if the given type is an input type. From ea03716e77ef98148979f2da298a7c5569777494 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Mon, 16 Mar 2026 15:53:52 +0100 Subject: [PATCH 3/4] Apply feedback --- src/Type/Definition/Type.php | 3 +++ src/Utils/TypeComparators.php | 25 +++++++++++---------- tests/Type/Definition/TypeTest.php | 35 +++++++++++++++++++++++++++++ tests/Utils/TypeComparatorsTest.php | 26 +++++++++++++++++++++ 4 files changed, 77 insertions(+), 12 deletions(-) diff --git a/src/Type/Definition/Type.php b/src/Type/Definition/Type.php index 68aed12a9..8a9b05647 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -216,6 +216,9 @@ public static function overrideStandardTypes(array $types): void /** * Determines if the given type is a built-in scalar (Int, Float, String, Boolean, ID). * + * Does not unwrap NonNull/List wrappers — checks the type instance directly. + * ScalarType is a NamedType, so {@see Type::getNamedType()} is unnecessary. + * * @param mixed $type * * @phpstan-assert-if-true ScalarType $type diff --git a/src/Utils/TypeComparators.php b/src/Utils/TypeComparators.php index cb1a455e1..3280a4690 100644 --- a/src/Utils/TypeComparators.php +++ b/src/Utils/TypeComparators.php @@ -19,12 +19,7 @@ public static function isEqualType(Type $typeA, Type $typeB): bool return true; } - // Built-in scalars may exist as different instances when a type loader - // overrides them. Compare by name to handle this case. - if (Type::isBuiltInScalar($typeA) - && Type::isBuiltInScalar($typeB) - && $typeA->name() === $typeB->name() - ) { + if (self::areSameBuiltInScalar($typeA, $typeB)) { return true; } @@ -55,12 +50,7 @@ public static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type return true; } - // Built-in scalars may exist as different instances when a type loader - // overrides them. Compare by name to handle this case. - if (Type::isBuiltInScalar($maybeSubType) - && Type::isBuiltInScalar($superType) - && $maybeSubType->name() === $superType->name() - ) { + if (self::areSameBuiltInScalar($maybeSubType, $superType)) { return true; } @@ -102,4 +92,15 @@ public static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type return false; } + + /** + * Built-in scalars may exist as different instances when a type loader + * overrides them. Compare by name to handle this case. + */ + private static function areSameBuiltInScalar(Type $typeA, Type $typeB): bool + { + return Type::isBuiltInScalar($typeA) + && Type::isBuiltInScalar($typeB) + && $typeA->name() === $typeB->name(); + } } diff --git a/tests/Type/Definition/TypeTest.php b/tests/Type/Definition/TypeTest.php index 4396e311b..45f1a75f1 100644 --- a/tests/Type/Definition/TypeTest.php +++ b/tests/Type/Definition/TypeTest.php @@ -2,6 +2,9 @@ namespace GraphQL\Tests\Type\Definition; +use GraphQL\Type\Definition\CustomScalarType; +use GraphQL\Type\Definition\EnumType; +use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use PHPUnit\Framework\TestCase; @@ -13,4 +16,36 @@ public function testWrappingNonNullableTypeWithNonNull(): void self::assertSame($nonNullableString, Type::nonNull($nonNullableString)); } + + public function testIsBuiltInScalarReturnsTrueForBuiltInScalars(): void + { + self::assertTrue(Type::isBuiltInScalar(Type::int())); // @phpstan-ignore staticMethod.alreadyNarrowedType + self::assertTrue(Type::isBuiltInScalar(Type::float())); // @phpstan-ignore staticMethod.alreadyNarrowedType + self::assertTrue(Type::isBuiltInScalar(Type::string())); // @phpstan-ignore staticMethod.alreadyNarrowedType + self::assertTrue(Type::isBuiltInScalar(Type::boolean())); // @phpstan-ignore staticMethod.alreadyNarrowedType + self::assertTrue(Type::isBuiltInScalar(Type::id())); // @phpstan-ignore staticMethod.alreadyNarrowedType + } + + public function testIsBuiltInScalarReturnsTrueForCustomScalarWithBuiltInName(): void + { + self::assertTrue(Type::isBuiltInScalar(new CustomScalarType(['name' => Type::STRING]))); // @phpstan-ignore staticMethod.alreadyNarrowedType + self::assertTrue(Type::isBuiltInScalar(new CustomScalarType(['name' => Type::ID]))); // @phpstan-ignore staticMethod.alreadyNarrowedType + } + + public function testIsBuiltInScalarReturnsFalseForNonScalarTypes(): void + { + self::assertFalse(Type::isBuiltInScalar(new ObjectType(['name' => 'Obj', 'fields' => []]))); // @phpstan-ignore staticMethod.impossibleType + self::assertFalse(Type::isBuiltInScalar(new EnumType(['name' => 'E', 'values' => ['A']]))); // @phpstan-ignore staticMethod.impossibleType + } + + public function testIsBuiltInScalarReturnsFalseForWrappedTypes(): void + { + self::assertFalse(Type::isBuiltInScalar(Type::nonNull(Type::string()))); // @phpstan-ignore staticMethod.impossibleType + self::assertFalse(Type::isBuiltInScalar(Type::listOf(Type::string()))); // @phpstan-ignore staticMethod.impossibleType + } + + public function testIsBuiltInScalarReturnsFalseForCustomScalarWithNonBuiltInName(): void + { + self::assertFalse(Type::isBuiltInScalar(new CustomScalarType(['name' => 'MyScalar']))); // @phpstan-ignore staticMethod.alreadyNarrowedType + } } diff --git a/tests/Utils/TypeComparatorsTest.php b/tests/Utils/TypeComparatorsTest.php index b7afb05cf..aa6f4667c 100644 --- a/tests/Utils/TypeComparatorsTest.php +++ b/tests/Utils/TypeComparatorsTest.php @@ -55,6 +55,32 @@ public function testIsTypeSubTypeOfWithWrappedDifferentInstances(): void )); } + public function testIsEqualTypeReturnsFalseForDifferentBuiltInScalars(): void + { + $customString = new CustomScalarType(['name' => Type::STRING]); + + self::assertFalse(TypeComparators::isEqualType(Type::string(), Type::int())); + self::assertFalse(TypeComparators::isEqualType($customString, Type::int())); + self::assertFalse(TypeComparators::isEqualType(Type::nonNull(Type::string()), Type::nonNull(Type::int()))); + } + + public function testIsEqualTypeReturnsFalseForDifferentWrapping(): void + { + $customString = new CustomScalarType(['name' => Type::STRING]); + + self::assertFalse(TypeComparators::isEqualType(Type::nonNull($customString), Type::string())); + self::assertFalse(TypeComparators::isEqualType(Type::listOf($customString), Type::string())); + } + + public function testIsTypeSubTypeOfReturnsFalseForDifferentBuiltInScalars(): void + { + $schema = $this->createSchemaWithCustomString(); + $customString = new CustomScalarType(['name' => Type::STRING]); + + self::assertFalse(TypeComparators::isTypeSubTypeOf($schema, $customString, Type::int())); + self::assertFalse(TypeComparators::isTypeSubTypeOf($schema, Type::int(), $customString)); + } + /** @throws InvariantViolation */ private function createSchemaWithCustomString(): Schema { From 033a63fd3bf4a49b77ccde7b5374524cc7b12522 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:55:27 +0000 Subject: [PATCH 4/4] Autofix --- docs/class-reference.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/class-reference.md b/docs/class-reference.md index 5a0bc9004..21b10dc2c 100644 --- a/docs/class-reference.md +++ b/docs/class-reference.md @@ -326,6 +326,9 @@ static function builtInScalars(): array /** * Determines if the given type is a built-in scalar (Int, Float, String, Boolean, ID). * + * Does not unwrap NonNull/List wrappers — checks the type instance directly. + * ScalarType is a NamedType, so {@see Type::getNamedType()} is unnecessary. + * * @param mixed $type * * @phpstan-assert-if-true ScalarType $type