Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support of SDL to KnownTypeNames validation rule #999

Merged
merged 18 commits into from
Mar 14, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ You can find and compare releases at the [GitHub release page](https://github.co
- Add ability to remove custom validation rules after adding them via `DocumentValidator::removeRule()`
- Allow lazy enum values
- Make `Node` implement `JsonSerializable`
- Add support for SDL validation to `KnownTypeNames` rule (#999)

### Optimized

Expand Down
17 changes: 4 additions & 13 deletions src/Utils/BuildSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,26 +217,17 @@ static function (string $typeName): Type {
}

/**
* @throws Error
*
* @return array<string, string>
*/
private function getOperationTypes(SchemaDefinitionNode $schemaDef): array
{
$opTypes = [];

/** @var array<string, string> $operationTypes */
$operationTypes = [];
foreach ($schemaDef->operationTypes as $operationType) {
$typeName = $operationType->type->name->value;
$operation = $operationType->operation;

if (! isset($this->nodeMap[$typeName])) {
throw new Error('Specified ' . $operation . ' type "' . $typeName . '" not found in document.');
}

$opTypes[$operation] = $typeName;
$operationTypes[$operationType->operation] = $operationType->type->name->value;
}

return $opTypes;
return $operationTypes;
}

public static function unknownType(string $typeName): Error
Expand Down
7 changes: 4 additions & 3 deletions src/Utils/SchemaExtender.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use function array_merge;
use function count;
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\EnumTypeExtensionNode;
Expand Down Expand Up @@ -621,11 +622,11 @@ public static function extend(
$typeDefinitionMap,
static function (string $typeName) use ($schema): Type {
$existingType = $schema->getType($typeName);
if (null !== $existingType) {
return static::extendNamedType($existingType);
if (null === $existingType) {
throw new InvariantViolation('Unknown type: "' . $typeName . '".');
}

throw new Error('Unknown type: "' . $typeName . '". Ensure that this type exists either in the original schema, or is added in a type definition.');
return static::extendNamedType($existingType);
},
$typeConfigDecorator
);
Expand Down
1 change: 1 addition & 0 deletions src/Validator/DocumentValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ public static function sdlRules(): array
return self::$sdlRules ??= [
LoneSchemaDefinition::class => new LoneSchemaDefinition(),
UniqueOperationTypes::class => new UniqueOperationTypes(),
KnownTypeNames::class => new KnownTypeNames(),
KnownDirectives::class => new KnownDirectives(),
KnownArgumentNamesOnDirectives::class => new KnownArgumentNamesOnDirectives(),
UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
Expand Down
88 changes: 71 additions & 17 deletions src/Validator/Rules/KnownTypeNames.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
namespace GraphQL\Validator\Rules;

use function array_keys;
use Closure;
use function count;
use GraphQL\Error\Error;
use GraphQL\Language\AST\NamedTypeNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Visitor;
use GraphQL\Language\VisitorOperation;
use GraphQL\Language\AST\TypeDefinitionNode;
use GraphQL\Language\AST\TypeSystemDefinitionNode;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils\Utils;
use GraphQL\Validator\SDLValidationContext;
use GraphQL\Validator\ValidationContext;
use function in_array;

/**
* Known type names.
Expand All @@ -22,30 +27,69 @@ class KnownTypeNames extends ValidationRule
{
public function getVisitor(ValidationContext $context): array
{
$skip = static function (): VisitorOperation {
return Visitor::skipNode();
};
return $this->getVisitorInternal($context);
}

public function getSDLVisitor(SDLValidationContext $context): array
{
return $this->getVisitorInternal($context);
}

/**
* @param ValidationContext|SDLValidationContext $context
*
* @return Closure[]
vhenzl marked this conversation as resolved.
Show resolved Hide resolved
*/
private function getVisitorInternal($context): array
vhenzl marked this conversation as resolved.
Show resolved Hide resolved
{
/** @var array<int, string> $definedTypes */
$definedTypes = [];
foreach ($context->getDocument()->definitions as $def) {
if (! ($def instanceof TypeDefinitionNode)) {
continue;
}

$definedTypes[] = $def->name->value;
vhenzl marked this conversation as resolved.
Show resolved Hide resolved
}

$standardTypeNames = array_keys(Type::getAllBuiltInTypes());

return [
// TODO: when validating IDL, re-enable these. Experimental version does not
// add unreferenced types, resulting in false-positive errors. Squelched
// errors for now.
NodeKind::OBJECT_TYPE_DEFINITION => $skip,
NodeKind::INTERFACE_TYPE_DEFINITION => $skip,
NodeKind::UNION_TYPE_DEFINITION => $skip,
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $skip,
NodeKind::NAMED_TYPE => static function (NamedTypeNode $node) use ($context): void {
$schema = $context->getSchema();
NodeKind::NAMED_TYPE => static function (NamedTypeNode $node, $_1, $parent, $_2, $ancestors) use ($context, $definedTypes, $standardTypeNames): void {
$typeName = $node->name->value;
$type = $schema->getType($typeName);
if (null !== $type) {
$schema = $context->getSchema();

if (in_array($typeName, $definedTypes, true)) {
return;
}

if (null !== $schema && $schema->hasType($typeName)) {
return;
}

$definitionNode = $ancestors[2] ?? $parent;
$isSDL = null !== $definitionNode && self::isSDLNode($definitionNode);
if ($isSDL && in_array($typeName, $standardTypeNames, true)) {
return;
}

$existingTypesMap = null !== $schema
? $schema->getTypeMap()
: [];
$typeNames = [
...array_keys($existingTypesMap),
...$definedTypes,
];
$context->reportError(new Error(
static::unknownTypeMessage(
$typeName,
Utils::suggestionList($typeName, array_keys($schema->getTypeMap()))
Utils::suggestionList(
$typeName,
$isSDL
// TODO: order
spawnia marked this conversation as resolved.
Show resolved Hide resolved
? [...$typeNames, ...$standardTypeNames]
: $typeNames
)
),
[$node]
));
Expand All @@ -65,4 +109,14 @@ public static function unknownTypeMessage(string $type, array $suggestedTypes):

return $message;
}

/**
* @param Node|array<int, Node> $value
*/
public static function isSDLNode($value): bool
{
return $value instanceof Node
// TODO: should be (TypeSystemDefinitionNode || TypeSystemExtensionNode)
spawnia marked this conversation as resolved.
Show resolved Hide resolved
&& $value instanceof TypeSystemDefinitionNode;
}
}
172 changes: 0 additions & 172 deletions tests/Utils/BuildSchemaLegacyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
* but these changes to `graphql-js` haven't been reflected in `graphql-php` yet.
* TODO align with:
* - https://github.com/graphql/graphql-js/commit/257797a0ebdddd3da6e75b7c237fdc12a1a7c75a
* - https://github.com/graphql/graphql-js/commit/3b9ea61f2348215dee755f779caef83df749d2bb
* - https://github.com/graphql/graphql-js/commit/64a5c3448a201737f9218856786c51d66f2deabd.
*/
class BuildSchemaLegacyTest extends TestCase
Expand Down Expand Up @@ -149,177 +148,6 @@ interface Character {

// Describe: Failures

/**
* @see it('Unknown type referenced')
*/
public function testUnknownTypeReferenced(): void
{
$this->expectExceptionObject(BuildSchema::unknownType('Bar'));
$sdl = '
schema {
query: Hello
}

type Hello {
bar: Bar
}
';
$doc = Parser::parse($sdl);
$schema = BuildSchema::buildAST($doc);
$schema->getTypeMap();
}

/**
* @see it('Unknown type in interface list')
*/
public function testUnknownTypeInInterfaceList(): void
{
$this->expectExceptionObject(BuildSchema::unknownType('Bar'));
$sdl = '
type Query implements Bar {
field: String
}
';
$doc = Parser::parse($sdl);
$schema = BuildSchema::buildAST($doc);
$schema->getTypeMap();
}

/**
* @see it('Unknown type in union list')
*/
public function testUnknownTypeInUnionList(): void
{
$this->expectExceptionObject(BuildSchema::unknownType('Bar'));
$sdl = '
union TestUnion = Bar
type Query { testUnion: TestUnion }
';
$doc = Parser::parse($sdl);
$schema = BuildSchema::buildAST($doc);
$schema->getTypeMap();
}

/**
* @see it('Unknown query type')
*/
public function testUnknownQueryType(): void
{
$this->expectException(Error::class);
$this->expectExceptionMessage('Specified query type "Wat" not found in document.');
$sdl = '
schema {
query: Wat
}

type Hello {
str: String
}
';
$doc = Parser::parse($sdl);
BuildSchema::buildAST($doc);
}

/**
* @see it('Unknown mutation type')
*/
public function testUnknownMutationType(): void
{
$this->expectException(Error::class);
$this->expectExceptionMessage('Specified mutation type "Wat" not found in document.');
$sdl = '
schema {
query: Hello
mutation: Wat
}

type Hello {
str: String
}
';
$doc = Parser::parse($sdl);
BuildSchema::buildAST($doc);
}

/**
* @see it('Unknown subscription type')
*/
public function testUnknownSubscriptionType(): void
{
$this->expectException(Error::class);
$this->expectExceptionMessage('Specified subscription type "Awesome" not found in document.');
$sdl = '
schema {
query: Hello
mutation: Wat
subscription: Awesome
}

type Hello {
str: String
}

type Wat {
str: String
}
';
$doc = Parser::parse($sdl);
BuildSchema::buildAST($doc);
}

/**
* @see it('Does not consider directive names')
*/
public function testDoesNotConsiderDirectiveNames(): void
{
$sdl = '
schema {
query: Foo
}

directive @Foo on QUERY
';
$doc = Parser::parse($sdl);
$this->expectExceptionMessage('Specified query type "Foo" not found in document.');
BuildSchema::build($doc);
}

/**
* @see it('Does not consider operation names')
*/
public function testDoesNotConsiderOperationNames(): void
{
$this->expectException(Error::class);
$this->expectExceptionMessage('Specified query type "Foo" not found in document.');
$sdl = '
schema {
query: Foo
}

query Foo { field }
';
$doc = Parser::parse($sdl);
BuildSchema::buildAST($doc);
}

/**
* @see it('Does not consider fragment names')
*/
public function testDoesNotConsiderFragmentNames(): void
{
$this->expectException(Error::class);
$this->expectExceptionMessage('Specified query type "Foo" not found in document.');
$sdl = '
schema {
query: Foo
}

fragment Foo on Type { field }
';
$doc = Parser::parse($sdl);
BuildSchema::buildAST($doc);
}

/**
* @see it('Forbids duplicate type definitions')
*/
Expand Down
Loading