<?php

declare(strict_types=1);

namespace GraphQL\Utils;

use GraphQL\Error\Error;
use GraphQL\Language\Printer;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Introspection;
use GraphQL\Type\Schema;
use function array_filter;
use function array_keys;
use function array_map;
use function array_merge;
use function array_values;
use function count;
use function explode;
use function implode;
use function ksort;
use function mb_strlen;
use function preg_match_all;
use function sprintf;
use function str_replace;
use function strlen;
use function substr;

/**
 * Given an instance of Schema, prints it in GraphQL type language.
 */
class SchemaPrinter
{
    /**
     * Accepts options as a second argument:
     *
     *    - commentDescriptions:
     *        Provide true to use preceding comments as the description.
     *
     * @param bool[] $options
     *
     * @api
     */
    public static function doPrint(Schema $schema, array $options = []) : string
    {
        return self::printFilteredSchema(
            $schema,
            static function ($type) {
                return ! Directive::isSpecifiedDirective($type);
            },
            static function ($type) {
                return ! Type::isBuiltInType($type);
            },
            $options
        );
    }

    /**
     * @param bool[] $options
     */
    private static function printFilteredSchema(Schema $schema, $directiveFilter, $typeFilter, $options) : string
    {
        $directives = array_filter(
            $schema->getDirectives(),
            static function ($directive) use ($directiveFilter) {
                return $directiveFilter($directive);
            }
        );

        $types = $schema->getTypeMap();
        ksort($types);
        $types = array_filter($types, $typeFilter);

        return sprintf(
            "%s\n",
            implode(
                "\n\n",
                array_filter(
                    array_merge(
                        [self::printSchemaDefinition($schema)],
                        array_map(
                            static function ($directive) use ($options) {
                                return self::printDirective($directive, $options);
                            },
                            $directives
                        ),
                        array_map(
                            static function ($type) use ($options) {
                                return self::printType($type, $options);
                            },
                            $types
                        )
                    )
                )
            )
        );
    }

    private static function printSchemaDefinition(Schema $schema)
    {
        if (self::isSchemaOfCommonNames($schema)) {
            return;
        }

        $operationTypes = [];

        $queryType = $schema->getQueryType();
        if ($queryType) {
            $operationTypes[] = sprintf('  query: %s', $queryType->name);
        }

        $mutationType = $schema->getMutationType();
        if ($mutationType) {
            $operationTypes[] = sprintf('  mutation: %s', $mutationType->name);
        }

        $subscriptionType = $schema->getSubscriptionType();
        if ($subscriptionType) {
            $operationTypes[] = sprintf('  subscription: %s', $subscriptionType->name);
        }

        return sprintf("schema {\n%s\n}", implode("\n", $operationTypes));
    }

    /**
     * GraphQL schema define root types for each type of operation. These types are
     * the same as any other type and can be named in any manner, however there is
     * a common naming convention:
     *
     *   schema {
     *     query: Query
     *     mutation: Mutation
     *   }
     *
     * When using this naming convention, the schema description can be omitted.
     */
    private static function isSchemaOfCommonNames(Schema $schema)
    {
        $queryType = $schema->getQueryType();
        if ($queryType && $queryType->name !== 'Query') {
            return false;
        }

        $mutationType = $schema->getMutationType();
        if ($mutationType && $mutationType->name !== 'Mutation') {
            return false;
        }

        $subscriptionType = $schema->getSubscriptionType();

        return ! $subscriptionType || $subscriptionType->name === 'Subscription';
    }

    private static function printDirective($directive, $options) : string
    {
        return self::printDescription($options, $directive) .
            'directive @' . $directive->name . self::printArgs($options, $directive->args) .
            ' on ' . implode(' | ', $directive->locations);
    }

    private static function printDescription($options, $def, $indentation = '', $firstInBlock = true) : string
    {
        if (! $def->description) {
            return '';
        }
        $lines = self::descriptionLines($def->description, 120 - strlen($indentation));
        if (isset($options['commentDescriptions'])) {
            return self::printDescriptionWithComments($lines, $indentation, $firstInBlock);
        }

        $description = $indentation && ! $firstInBlock
            ? "\n" . $indentation . '"""'
            : $indentation . '"""';

        // In some circumstances, a single line can be used for the description.
        if (count($lines) === 1 &&
            mb_strlen($lines[0]) < 70 &&
            substr($lines[0], -1) !== '"'
        ) {
            return $description . self::escapeQuote($lines[0]) . "\"\"\"\n";
        }

        // Format a multi-line block quote to account for leading space.
        $hasLeadingSpace = isset($lines[0]) &&
            (
                substr($lines[0], 0, 1) === ' ' ||
                substr($lines[0], 0, 1) === '\t'
            );
        if (! $hasLeadingSpace) {
            $description .= "\n";
        }

        $lineLength = count($lines);
        for ($i = 0; $i < $lineLength; $i++) {
            if ($i !== 0 || ! $hasLeadingSpace) {
                $description .= $indentation;
            }
            $description .= self::escapeQuote($lines[$i]) . "\n";
        }
        $description .= $indentation . "\"\"\"\n";

        return $description;
    }

    /**
     * @return string[]
     */
    private static function descriptionLines(string $description, int $maxLen) : array
    {
        $lines    = [];
        $rawLines = explode("\n", $description);
        foreach ($rawLines as $line) {
            if ($line === '') {
                $lines[] = $line;
            } else {
                // For > 120 character long lines, cut at space boundaries into sublines
                // of ~80 chars.
                $sublines = self::breakLine($line, $maxLen);
                foreach ($sublines as $subline) {
                    $lines[] = $subline;
                }
            }
        }

        return $lines;
    }

    /**
     * @return string[]
     */
    private static function breakLine(string $line, int $maxLen) : array
    {
        if (strlen($line) < $maxLen + 5) {
            return [$line];
        }
        preg_match_all('/((?: |^).{15,' . ($maxLen - 40) . '}(?= |$))/', $line, $parts);
        $parts = $parts[0];

        return array_map('trim', $parts);
    }

    private static function printDescriptionWithComments($lines, $indentation, $firstInBlock) : string
    {
        $description = $indentation && ! $firstInBlock ? "\n" : '';
        foreach ($lines as $line) {
            if ($line === '') {
                $description .= $indentation . "#\n";
            } else {
                $description .= $indentation . '# ' . $line . "\n";
            }
        }

        return $description;
    }

    private static function escapeQuote($line) : string
    {
        return str_replace('"""', '\\"""', $line);
    }

    private static function printArgs($options, $args, $indentation = '') : string
    {
        if (! $args) {
            return '';
        }

        // If every arg does not have a description, print them on one line.
        if (Utils::every(
            $args,
            static function ($arg) {
                return empty($arg->description);
            }
        )) {
            return '(' . implode(', ', array_map('self::printInputValue', $args)) . ')';
        }

        return sprintf(
            "(\n%s\n%s)",
            implode(
                "\n",
                array_map(
                    static function ($arg, $i) use ($indentation, $options) {
                        return self::printDescription($options, $arg, '  ' . $indentation, ! $i) . '  ' . $indentation .
                            self::printInputValue($arg);
                    },
                    $args,
                    array_keys($args)
                )
            ),
            $indentation
        );
    }

    private static function printInputValue($arg) : string
    {
        $argDecl = $arg->name . ': ' . (string) $arg->getType();
        if ($arg->defaultValueExists()) {
            $argDecl .= ' = ' . Printer::doPrint(AST::astFromValue($arg->defaultValue, $arg->getType()));
        }

        return $argDecl;
    }

    /**
     * @param bool[] $options
     */
    public static function printType(Type $type, array $options = []) : string
    {
        if ($type instanceof ScalarType) {
            return self::printScalar($type, $options);
        }

        if ($type instanceof ObjectType) {
            return self::printObject($type, $options);
        }

        if ($type instanceof InterfaceType) {
            return self::printInterface($type, $options);
        }

        if ($type instanceof UnionType) {
            return self::printUnion($type, $options);
        }

        if ($type instanceof EnumType) {
            return self::printEnum($type, $options);
        }

        if ($type instanceof InputObjectType) {
            return self::printInputObject($type, $options);
        }

        throw new Error(sprintf('Unknown type: %s.', Utils::printSafe($type)));
    }

    /**
     * @param bool[] $options
     */
    private static function printScalar(ScalarType $type, array $options) : string
    {
        return sprintf('%sscalar %s', self::printDescription($options, $type), $type->name);
    }

    /**
     * @param bool[] $options
     */
    private static function printObject(ObjectType $type, array $options) : string
    {
        $interfaces            = $type->getInterfaces();
        $implementedInterfaces = ! empty($interfaces) ?
            ' implements ' . implode(
                ' & ',
                array_map(
                    static function ($i) {
                        return $i->name;
                    },
                    $interfaces
                )
            ) : '';

        return self::printDescription($options, $type) .
            sprintf("type %s%s {\n%s\n}", $type->name, $implementedInterfaces, self::printFields($options, $type));
    }

    /**
     * @param bool[] $options
     */
    private static function printFields($options, $type) : string
    {
        $fields = array_values($type->getFields());

        return implode(
            "\n",
            array_map(
                static function ($f, $i) use ($options) {
                    return self::printDescription($options, $f, '  ', ! $i) . '  ' .
                        $f->name . self::printArgs($options, $f->args, '  ') . ': ' .
                        (string) $f->getType() . self::printDeprecated($f);
                },
                $fields,
                array_keys($fields)
            )
        );
    }

    private static function printDeprecated($fieldOrEnumVal) : string
    {
        $reason = $fieldOrEnumVal->deprecationReason;
        if (empty($reason)) {
            return '';
        }
        if ($reason === '' || $reason === Directive::DEFAULT_DEPRECATION_REASON) {
            return ' @deprecated';
        }

        return ' @deprecated(reason: ' .
            Printer::doPrint(AST::astFromValue($reason, Type::string())) . ')';
    }

    /**
     * @param bool[] $options
     */
    private static function printInterface(InterfaceType $type, array $options) : string
    {
        return self::printDescription($options, $type) .
            sprintf("interface %s {\n%s\n}", $type->name, self::printFields($options, $type));
    }

    /**
     * @param bool[] $options
     */
    private static function printUnion(UnionType $type, array $options) : string
    {
        return self::printDescription($options, $type) .
            sprintf('union %s = %s', $type->name, implode(' | ', $type->getTypes()));
    }

    /**
     * @param bool[] $options
     */
    private static function printEnum(EnumType $type, array $options) : string
    {
        return self::printDescription($options, $type) .
            sprintf("enum %s {\n%s\n}", $type->name, self::printEnumValues($type->getValues(), $options));
    }

    /**
     * @param bool[] $options
     */
    private static function printEnumValues($values, $options) : string
    {
        return implode(
            "\n",
            array_map(
                static function ($value, $i) use ($options) {
                    return self::printDescription($options, $value, '  ', ! $i) . '  ' .
                        $value->name . self::printDeprecated($value);
                },
                $values,
                array_keys($values)
            )
        );
    }

    /**
     * @param bool[] $options
     */
    private static function printInputObject(InputObjectType $type, array $options) : string
    {
        $fields = array_values($type->getFields());

        return self::printDescription($options, $type) .
            sprintf(
                "input %s {\n%s\n}",
                $type->name,
                implode(
                    "\n",
                    array_map(
                        static function ($f, $i) use ($options) {
                            return self::printDescription($options, $f, '  ', ! $i) . '  ' . self::printInputValue($f);
                        },
                        $fields,
                        array_keys($fields)
                    )
                )
            );
    }

    /**
     * @param bool[] $options
     *
     * @api
     */
    public static function printIntrospectionSchema(Schema $schema, array $options = []) : string
    {
        return self::printFilteredSchema(
            $schema,
            [Directive::class, 'isSpecifiedDirective'],
            [Introspection::class, 'isIntrospectionType'],
            $options
        );
    }
}


<?php
namespace GraphQL\Examples\Blog\Type\Scalar;

use GraphQL\Error\Error;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Utils\Utils;

class UrlType extends ScalarType
{
    /**
     * Serializes an internal value to include in a response.
     *
     * @param mixed $value
     * @return mixed
     */
    public function serialize($value)
    {
        // Assuming internal representation of url is always correct:
        return $value;

        // If it might be incorrect and you want to make sure that only correct values are included in response -
        // use following line instead:
        // return $this->parseValue($value);
    }

    /**
     * Parses an externally provided value (query variable) to use as an input
     *
     * @param mixed $value
     * @return mixed
     * @throws Error
     */
    public function parseValue($value)
    {
        if (!is_string($value) || !filter_var($value, FILTER_VALIDATE_URL)) { // quite naive, but after all this is example
            throw new Error("Cannot represent value as URL: " . Utils::printSafe($value));
        }
        return $value;
    }

    /**
     * Parses an externally provided literal value to use as an input (e.g. in Query AST)
     *
     * @param Node $valueNode
     * @param array|null $variables
     * @return null|string
     * @throws Error
     */
    public function parseLiteral($valueNode, array $variables = null)
    {
        // Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL
        // error location in query:
        if (!($valueNode instanceof StringValueNode)) {
            throw new Error('Query error: Can only parse strings got: ' . $valueNode->kind, [$valueNode]);
        }
        if (!is_string($valueNode->value) || !filter_var($valueNode->value, FILTER_VALIDATE_URL)) {
            throw new Error('Query error: Not a valid URL', [$valueNode]);
        }
        return $valueNode->value;
    }
}


<?php

declare(strict_types=1);

namespace GraphQL\Type\Definition;

use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Type\Schema;
use function array_merge_recursive;

/**
 * Structure containing information useful for field resolution process.
 * Passed as 3rd argument to every field resolver. See [docs on field resolving (data fetching)](data-fetching.md).
 */
class ResolveInfo
{
    /**
     * The name of the field being resolved
     *
     * @api
     * @var string
     */
    public $fieldName;

    /**
     * AST of all nodes referencing this field in the query.
     *
     * @api
     * @var FieldNode[]
     */
    public $fieldNodes = [];

    /**
     * Expected return type of the field being resolved
     *
     * @api
     * @var ScalarType|ObjectType|InterfaceType|UnionType|EnumType|ListOfType|NonNull
     */
    public $returnType;

    /**
     * Parent type of the field being resolved
     *
     * @api
     * @var ObjectType
     */
    public $parentType;

    /**
     * Path to this field from the very root value
     *
     * @api
     * @var string[][]
     */
    public $path;

    /**
     * Instance of a schema used for execution
     *
     * @api
     * @var Schema
     */
    public $schema;

    /**
     * AST of all fragments defined in query
     *
     * @api
     * @var FragmentDefinitionNode[]
     */
    public $fragments = [];

    /**
     * Root value passed to query execution
     *
     * @api
     * @var mixed|null
     */
    public $rootValue;

    /**
     * AST of operation definition node (query, mutation)
     *
     * @api
     * @var OperationDefinitionNode|null
     */
    public $operation;

    /**
     * Array of variables passed to query execution
     *
     * @api
     * @var mixed[]
     */
    public $variableValues = [];

    /** @var QueryPlan */
    private $queryPlan;

    /**
     * @param FieldNode[]                                                               $fieldNodes
     * @param ScalarType|ObjectType|InterfaceType|UnionType|EnumType|ListOfType|NonNull $returnType
     * @param string[][]                                                                $path
     * @param FragmentDefinitionNode[]                                                  $fragments
     * @param mixed|null                                                                $rootValue
     * @param mixed[]                                                                   $variableValues
     */
    public function __construct(
        string $fieldName,
        iterable $fieldNodes,
        $returnType,
        ObjectType $parentType,
        array $path,
        Schema $schema,
        array $fragments,
        $rootValue,
        ?OperationDefinitionNode $operation,
        array $variableValues
    ) {
        $this->fieldName      = $fieldName;
        $this->fieldNodes     = $fieldNodes;
        $this->returnType     = $returnType;
        $this->parentType     = $parentType;
        $this->path           = $path;
        $this->schema         = $schema;
        $this->fragments      = $fragments;
        $this->rootValue      = $rootValue;
        $this->operation      = $operation;
        $this->variableValues = $variableValues;
    }

    /**
     * Helper method that returns names of all fields selected in query for
     * $this->fieldName up to $depth levels
     *
     * Example:
     * query MyQuery{
     * {
     *   root {
     *     id,
     *     nested {
     *      nested1
     *      nested2 {
     *        nested3
     *      }
     *     }
     *   }
     * }
     *
     * Given this ResolveInfo instance is a part of "root" field resolution, and $depth === 1,
     * method will return:
     * [
     *     'id' => true,
     *     'nested' => [
     *         nested1 => true,
     *         nested2 => true
     *     ]
     * ]
     *
     * Warning: this method it is a naive implementation which does not take into account
     * conditional typed fragments. So use it with care for fields of interface and union types.
     *
     * @param int $depth How many levels to include in output
     *
     * @return bool[]
     *
     * @api
     */
    public function getFieldSelection($depth = 0)
    {
        $fields = [];

        /** @var FieldNode $fieldNode */
        foreach ($this->fieldNodes as $fieldNode) {
            $fields = array_merge_recursive($fields, $this->foldSelectionSet($fieldNode->selectionSet, $depth));
        }

        return $fields;
    }

    public function lookAhead() : QueryPlan
    {
        if ($this->queryPlan === null) {
            $this->queryPlan = new QueryPlan(
                $this->parentType,
                $this->schema,
                $this->fieldNodes,
                $this->variableValues,
                $this->fragments
            );
        }

        return $this->queryPlan;
    }

    /**
     * @return bool[]
     */
    private function foldSelectionSet(SelectionSetNode $selectionSet, int $descend) : array
    {
        $fields = [];
        foreach ($selectionSet->selections as $selectionNode) {
            if ($selectionNode instanceof FieldNode) {
                $fields[$selectionNode->name->value] = $descend > 0 && ! empty($selectionNode->selectionSet)
                    ? $this->foldSelectionSet($selectionNode->selectionSet, $descend - 1)
                    : true;
            } 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),
                        $fields
                    );
                }
            } elseif ($selectionNode instanceof InlineFragmentNode) {
                $fields = array_merge_recursive(
                    $this->foldSelectionSet($selectionNode->selectionSet, $descend),
                    $fields
                );
            }
        }

        return $fields;
    }
}


<?php

declare(strict_types=1);

namespace GraphQL\Validator\Rules;

use GraphQL\Error\Error;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Utils\TypeComparators;
use GraphQL\Utils\TypeInfo;
use GraphQL\Validator\ValidationContext;
use function sprintf;

class VariablesInAllowedPosition extends ValidationRule
{
    /** @var */
    public $varDefMap;

    public function getVisitor(ValidationContext $context)
    {
        return [
            NodeKind::OPERATION_DEFINITION => [
                'enter' => function () {
                    $this->varDefMap = [];
                },
                'leave' => function (OperationDefinitionNode $operation) use ($context) {
                    $usages = $context->getRecursiveVariableUsages($operation);

                    foreach ($usages as $usage) {
                        $node    = $usage['node'];
                        $type    = $usage['type'];
                        $varName = $node->name->value;
                        $varDef  = $this->varDefMap[$varName] ?? null;

                        if ($varDef === null || $type === null) {
                            continue;
                        }

                        // A var type is allowed if it is the same or more strict (e.g. is
                        // a subtype of) than the expected type. It can be more strict if
                        // the variable type is non-null when the expected type is nullable.
                        // If both are list types, the variable item type can be more strict
                        // than the expected item type (contravariant).
                        $schema  = $context->getSchema();
                        $varType = TypeInfo::typeFromAST($schema, $varDef->type);

                        if (! $varType || TypeComparators::isTypeSubTypeOf(
                            $schema,
                            $this->effectiveType($varType, $varDef),
                            $type
                        )) {
                            continue;
                        }

                        $context->reportError(new Error(
                            self::badVarPosMessage($varName, $varType, $type),
                            [$varDef, $node]
                        ));
                    }
                },
            ],
            NodeKind::VARIABLE_DEFINITION  => function (VariableDefinitionNode $varDefNode) {
                $this->varDefMap[$varDefNode->variable->name->value] = $varDefNode;
            },
        ];
    }

    private function effectiveType($varType, $varDef)
    {
        return ! $varDef->defaultValue || $varType instanceof NonNull ? $varType : new NonNull($varType);
    }

    /**
     * A var type is allowed if it is the same or more strict than the expected
     * type. It can be more strict if the variable type is non-null when the
     * expected type is nullable. If both are list types, the variable item type can
     * be more strict than the expected item type.
     */
    public static function badVarPosMessage($varName, $varType, $expectedType)
    {
        return sprintf(
            'Variable "$%s" of type "%s" used in position expecting type "%s".',
            $varName,
            $varType,
            $expectedType
        );
    }

    /** If a variable definition has a default value, it's effectively non-null. */
    private function varTypeAllowedForType($varType, $expectedType)
    {
        if ($expectedType instanceof NonNull) {
            if ($varType instanceof NonNull) {
                return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType->getWrappedType());
            }

            return false;
        }
        if ($varType instanceof NonNull) {
            return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType);
        }
        if ($varType instanceof ListOfType && $expectedType instanceof ListOfType) {
            return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType->getWrappedType());
        }

        return $varType === $expectedType;
    }
}


<?php

declare(strict_types=1);

/**
 * Utility for finding breaking/dangerous changes between two schemas.
 */

namespace GraphQL\Utils;

use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NamedType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Schema;
use TypeError;
use function array_flip;
use function array_key_exists;
use function array_keys;
use function array_merge;
use function class_alias;
use function sprintf;

class BreakingChangesFinder
{
    public const BREAKING_CHANGE_FIELD_CHANGED_KIND            = 'FIELD_CHANGED_KIND';
    public const BREAKING_CHANGE_FIELD_REMOVED                 = 'FIELD_REMOVED';
    public const BREAKING_CHANGE_TYPE_CHANGED_KIND             = 'TYPE_CHANGED_KIND';
    public const BREAKING_CHANGE_TYPE_REMOVED                  = 'TYPE_REMOVED';
    public const BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION       = 'TYPE_REMOVED_FROM_UNION';
    public const BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM       = 'VALUE_REMOVED_FROM_ENUM';
    public const BREAKING_CHANGE_ARG_REMOVED                   = 'ARG_REMOVED';
    public const BREAKING_CHANGE_ARG_CHANGED_KIND              = 'ARG_CHANGED_KIND';
    public const BREAKING_CHANGE_NON_NULL_ARG_ADDED            = 'NON_NULL_ARG_ADDED';
    public const BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED    = 'NON_NULL_INPUT_FIELD_ADDED';
    public const BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT = 'INTERFACE_REMOVED_FROM_OBJECT';
    public const BREAKING_CHANGE_DIRECTIVE_REMOVED             = 'DIRECTIVE_REMOVED';
    public const BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED         = 'DIRECTIVE_ARG_REMOVED';
    public const BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED    = 'DIRECTIVE_LOCATION_REMOVED';
    public const BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED  = 'NON_NULL_DIRECTIVE_ARG_ADDED';
    public const DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED    = 'ARG_DEFAULT_VALUE_CHANGE';
    public const DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM          = 'VALUE_ADDED_TO_ENUM';
    public const DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT    = 'INTERFACE_ADDED_TO_OBJECT';
    public const DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION          = 'TYPE_ADDED_TO_UNION';
    public const DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED   = 'NULLABLE_INPUT_FIELD_ADDED';
    public const DANGEROUS_CHANGE_NULLABLE_ARG_ADDED           = 'NULLABLE_ARG_ADDED';

    /**
     * Given two schemas, returns an Array containing descriptions of all the types
     * of breaking changes covered by the other functions down below.
     *
     * @return string[][]
     */
    public static function findBreakingChanges(Schema $oldSchema, Schema $newSchema)
    {
        return array_merge(
            self::findRemovedTypes($oldSchema, $newSchema),
            self::findTypesThatChangedKind($oldSchema, $newSchema),
            self::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema),
            self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['breakingChanges'],
            self::findTypesRemovedFromUnions($oldSchema, $newSchema),
            self::findValuesRemovedFromEnums($oldSchema, $newSchema),
            self::findArgChanges($oldSchema, $newSchema)['breakingChanges'],
            self::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema),
            self::findRemovedDirectives($oldSchema, $newSchema),
            self::findRemovedDirectiveArgs($oldSchema, $newSchema),
            self::findAddedNonNullDirectiveArgs($oldSchema, $newSchema),
            self::findRemovedDirectiveLocations($oldSchema, $newSchema)
        );
    }

    /**
     * Given two schemas, returns an Array containing descriptions of any breaking
     * changes in the newSchema related to removing an entire type.
     *
     * @return string[][]
     */
    public static function findRemovedTypes(
        Schema $oldSchema,
        Schema $newSchema
    ) {
        $oldTypeMap = $oldSchema->getTypeMap();
        $newTypeMap = $newSchema->getTypeMap();

        $breakingChanges = [];
        foreach (array_keys($oldTypeMap) as $typeName) {
            if (isset($newTypeMap[$typeName])) {
                continue;
            }

            $breakingChanges[] = [
                'type'        => self::BREAKING_CHANGE_TYPE_REMOVED,
                'description' => "${typeName} was removed.",
            ];
        }

        return $breakingChanges;
    }

    /**
     * Given two schemas, returns an Array containing descriptions of any breaking
     * changes in the newSchema related to changing the type of a type.
     *
     * @return string[][]
     */
    public static function findTypesThatChangedKind(
        Schema $schemaA,
        Schema $schemaB
    ) : iterable {
        $schemaATypeMap = $schemaA->getTypeMap();
        $schemaBTypeMap = $schemaB->getTypeMap();

        $breakingChanges = [];
        foreach ($schemaATypeMap as $typeName => $schemaAType) {
            if (! isset($schemaBTypeMap[$typeName])) {
                continue;
            }
            $schemaBType = $schemaBTypeMap[$typeName];
            if ($schemaAType instanceof $schemaBType) {
                continue;
            }

            if ($schemaBType instanceof $schemaAType) {
                continue;
            }

            $schemaATypeKindName = self::typeKindName($schemaAType);
            $schemaBTypeKindName = self::typeKindName($schemaBType);
            $breakingChanges[]   = [
                'type'        => self::BREAKING_CHANGE_TYPE_CHANGED_KIND,
                'description' => "${typeName} changed from ${schemaATypeKindName} to ${schemaBTypeKindName}.",
            ];
        }

        return $breakingChanges;
    }

    /**
     * @return string
     *
     * @throws TypeError
     */
    private static function typeKindName(Type $type)
    {
        if ($type instanceof ScalarType) {
            return 'a Scalar type';
        }

        if ($type instanceof ObjectType) {
            return 'an Object type';
        }

        if ($type instanceof InterfaceType) {
            return 'an Interface type';
        }

        if ($type instanceof UnionType) {
            return 'a Union type';
        }

        if ($type instanceof EnumType) {
            return 'an Enum type';
        }

        if ($type instanceof InputObjectType) {
            return 'an Input type';
        }

        throw new TypeError('unknown type ' . $type->name);
    }

    /**
     * @return string[][]
     */
    public static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes(
        Schema $oldSchema,
        Schema $newSchema
    ) {
        $oldTypeMap = $oldSchema->getTypeMap();
        $newTypeMap = $newSchema->getTypeMap();

        $breakingChanges = [];
        foreach ($oldTypeMap as $typeName => $oldType) {
            $newType = $newTypeMap[$typeName] ?? null;
            if (! ($oldType instanceof ObjectType || $oldType instanceof InterfaceType) ||
                ! ($newType instanceof ObjectType || $newType instanceof InterfaceType) ||
                ! ($newType instanceof $oldType)
            ) {
                continue;
            }

            $oldTypeFieldsDef = $oldType->getFields();
            $newTypeFieldsDef = $newType->getFields();
            foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) {
                // Check if the field is missing on the type in the new schema.
                if (! isset($newTypeFieldsDef[$fieldName])) {
                    $breakingChanges[] = [
                        'type'        => self::BREAKING_CHANGE_FIELD_REMOVED,
                        'description' => "${typeName}.${fieldName} was removed.",
                    ];
                } else {
                    $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
                    $newFieldType = $newTypeFieldsDef[$fieldName]->getType();
                    $isSafe       = self::isChangeSafeForObjectOrInterfaceField(
                        $oldFieldType,
                        $newFieldType
                    );
                    if (! $isSafe) {
                        $oldFieldTypeString = $oldFieldType instanceof NamedType
                            ? $oldFieldType->name
                            : $oldFieldType;
                        $newFieldTypeString = $newFieldType instanceof NamedType
                            ? $newFieldType->name
                            : $newFieldType;
                        $breakingChanges[]  = [
                            'type'        => self::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                            'description' => "${typeName}.${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}.",
                        ];
                    }
                }
            }
        }

        return $breakingChanges;
    }

    /**
     * @return bool
     */
    private static function isChangeSafeForObjectOrInterfaceField(
        Type $oldType,
        Type $newType
    ) {
        if ($oldType instanceof NamedType) {
            return // if they're both named types, see if their names are equivalent
                ($newType instanceof NamedType && $oldType->name === $newType->name) ||
                // moving from nullable to non-null of the same underlying type is safe
                ($newType instanceof NonNull &&
                    self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType())
                );
        }

        if ($oldType instanceof ListOfType) {
            return // if they're both lists, make sure the underlying types are compatible
                ($newType instanceof ListOfType &&
                    self::isChangeSafeForObjectOrInterfaceField(
                        $oldType->getWrappedType(),
                        $newType->getWrappedType()
                    )) ||
                // moving from nullable to non-null of the same underlying type is safe
                ($newType instanceof NonNull &&
                    self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType()));
        }

        if ($oldType instanceof NonNull) {
            // if they're both non-null, make sure the underlying types are compatible
            return $newType instanceof NonNull &&
                self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType());
        }

        return false;
    }

    /**
     * @return string[][]
     */
    public static function findFieldsThatChangedTypeOnInputObjectTypes(
        Schema $oldSchema,
        Schema $newSchema
    ) {
        $oldTypeMap = $oldSchema->getTypeMap();
        $newTypeMap = $newSchema->getTypeMap();

        $breakingChanges  = [];
        $dangerousChanges = [];
        foreach ($oldTypeMap as $typeName => $oldType) {
            $newType = $newTypeMap[$typeName] ?? null;
            if (! ($oldType instanceof InputObjectType) || ! ($newType instanceof InputObjectType)) {
                continue;
            }

            $oldTypeFieldsDef = $oldType->getFields();
            $newTypeFieldsDef = $newType->getFields();
            foreach (array_keys($oldTypeFieldsDef) as $fieldName) {
                if (! isset($newTypeFieldsDef[$fieldName])) {
                    $breakingChanges[] = [
                        'type'        => self::BREAKING_CHANGE_FIELD_REMOVED,
                        'description' => "${typeName}.${fieldName} was removed.",
                    ];
                } else {
                    $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
                    $newFieldType = $newTypeFieldsDef[$fieldName]->getType();

                    $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg(
                        $oldFieldType,
                        $newFieldType
                    );
                    if (! $isSafe) {
                        $oldFieldTypeString = $oldFieldType instanceof NamedType
                            ? $oldFieldType->name
                            : $oldFieldType;
                        $newFieldTypeString = $newFieldType instanceof NamedType
                            ? $newFieldType->name
                            : $newFieldType;
                        $breakingChanges[]  = [
                            'type'        => self::BREAKING_CHANGE_FIELD_CHANGED_KIND,
                            'description' => "${typeName}.${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}.",
                        ];
                    }
                }
            }
            // Check if a field was added to the input object type
            foreach ($newTypeFieldsDef as $fieldName => $fieldDef) {
                if (isset($oldTypeFieldsDef[$fieldName])) {
                    continue;
                }

                $newTypeName = $newType->name;
                if ($fieldDef->getType() instanceof NonNull) {
                    $breakingChanges[] = [
                        'type'        => self::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED,
                        'description' => "A non-null field ${fieldName} on input type ${newTypeName} was added.",
                    ];
                } else {
                    $dangerousChanges[] = [
                        'type'        => self::DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED,
                        'description' => "A nullable field ${fieldName} on input type ${newTypeName} was added.",
                    ];
                }
            }
        }

        return [
            'breakingChanges'  => $breakingChanges,
            'dangerousChanges' => $dangerousChanges,
        ];
    }

    /**
     * @return bool
     */
    private static function isChangeSafeForInputObjectFieldOrFieldArg(
        Type $oldType,
        Type $newType
    ) {
        if ($oldType instanceof NamedType) {
            // if they're both named types, see if their names are equivalent
            return $newType instanceof NamedType && $oldType->name === $newType->name;
        }

        if ($oldType instanceof ListOfType) {
            // if they're both lists, make sure the underlying types are compatible
            return $newType instanceof ListOfType &&
                self::isChangeSafeForInputObjectFieldOrFieldArg(
                    $oldType->getWrappedType(),
                    $newType->getWrappedType()
                );
        }

        if ($oldType instanceof NonNull) {
            return // if they're both non-null, make sure the underlying types are
                // compatible
                ($newType instanceof NonNull &&
                    self::isChangeSafeForInputObjectFieldOrFieldArg(
                        $oldType->getWrappedType(),
                        $newType->getWrappedType()
                    )) ||
                // moving from non-null to nullable of the same underlying type is safe
                ! ($newType instanceof NonNull) &&
                self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType);
        }

        return false;
    }

    /**
     * Given two schemas, returns an Array containing descriptions of any breaking
     * changes in the newSchema related to removing types from a union type.
     *
     * @return string[][]
     */
    public static function findTypesRemovedFromUnions(
        Schema $oldSchema,
        Schema $newSchema
    ) {
        $oldTypeMap = $oldSchema->getTypeMap();
        $newTypeMap = $newSchema->getTypeMap();

        $typesRemovedFromUnion = [];
        foreach ($oldTypeMap as $typeName => $oldType) {
            $newType = $newTypeMap[$typeName] ?? null;
            if (! ($oldType instanceof UnionType) || ! ($newType instanceof UnionType)) {
                continue;
            }
            $typeNamesInNewUnion = [];
            foreach ($newType->getTypes() as $type) {
                $typeNamesInNewUnion[$type->name] = true;
            }
            foreach ($oldType->getTypes() as $type) {
                if (isset($typeNamesInNewUnion[$type->name])) {
                    continue;
                }

                $typesRemovedFromUnion[] = [
                    'type'        => self::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION,
                    'description' => sprintf('%s was removed from union type %s.', $type->name, $typeName),
                ];
            }
        }

        return $typesRemovedFromUnion;
    }

    /**
     * Given two schemas, returns an Array containing descriptions of any breaking
     * changes in the newSchema related to removing values from an enum type.
     *
     * @return string[][]
     */
    public static function findValuesRemovedFromEnums(
        Schema $oldSchema,
        Schema $newSchema
    ) {
        $oldTypeMap = $oldSchema->getTypeMap();
        $newTypeMap = $newSchema->getTypeMap();

        $valuesRemovedFromEnums = [];
        foreach ($oldTypeMap as $typeName => $oldType) {
            $newType = $newTypeMap[$typeName] ?? null;
            if (! ($oldType instanceof EnumType) || ! ($newType instanceof EnumType)) {
                continue;
            }
            $valuesInNewEnum = [];
            foreach ($newType->getValues() as $value) {
                $valuesInNewEnum[$value->name] = true;
            }
            foreach ($oldType->getValues() as $value) {
                if (isset($valuesInNewEnum[$value->name])) {
                    continue;
                }

                $valuesRemovedFromEnums[] = [
                    'type'        => self::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM,
                    'description' => sprintf('%s was removed from enum type %s.', $value->name, $typeName),
                ];
            }
        }

        return $valuesRemovedFromEnums;
    }

    /**
     * Given two schemas, returns an Array containing descriptions of any
     * breaking or dangerous changes in the newSchema related to arguments
     * (such as removal or change of type of an argument, or a change in an
     * argument's default value).
     *
     * @return string[][]
     */
    public static function findArgChanges(
        Schema $oldSchema,
        Schema $newSchema
    ) {
        $oldTypeMap = $oldSchema->getTypeMap();
        $newTypeMap = $newSchema->getTypeMap();

        $breakingChanges  = [];
        $dangerousChanges = [];

        foreach ($oldTypeMap as $typeName => $oldType) {
            $newType = $newTypeMap[$typeName] ?? null;
            if (! ($oldType instanceof ObjectType || $oldType instanceof InterfaceType) ||
                ! ($newType instanceof ObjectType || $newType instanceof InterfaceType) ||
                ! ($newType instanceof $oldType)
            ) {
                continue;
            }

            $oldTypeFields = $oldType->getFields();
            $newTypeFields = $newType->getFields();

            foreach ($oldTypeFields as $fieldName => $oldField) {
                if (! isset($newTypeFields[$fieldName])) {
                    continue;
                }

                foreach ($oldField->args as $oldArgDef) {
                    $newArgs   = $newTypeFields[$fieldName]->args;
                    $newArgDef = Utils::find(
                        $newArgs,
                        static function ($arg) use ($oldArgDef) {
                            return $arg->name === $oldArgDef->name;
                        }
                    );
                    if ($newArgDef !== null) {
                        $isSafe     = self::isChangeSafeForInputObjectFieldOrFieldArg(
                            $oldArgDef->getType(),
                            $newArgDef->getType()
                        );
                        $oldArgType = $oldArgDef->getType();
                        $oldArgName = $oldArgDef->name;
                        if (! $isSafe) {
                            $newArgType        = $newArgDef->getType();
                            $breakingChanges[] = [
                                'type'        => self::BREAKING_CHANGE_ARG_CHANGED_KIND,
                                'description' => "${typeName}.${fieldName} arg ${oldArgName} has changed type from ${oldArgType} to ${newArgType}",
                            ];
                        } elseif ($oldArgDef->defaultValueExists() && $oldArgDef->defaultValue !== $newArgDef->defaultValue) {
                            $dangerousChanges[] = [
                                'type'        => self::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED,
                                'description' => "${typeName}.${fieldName} arg ${oldArgName} has changed defaultValue",
                            ];
                        }
                    } else {
                        $breakingChanges[] = [
                            'type'        => self::BREAKING_CHANGE_ARG_REMOVED,
                            'description' => sprintf(
                                '%s.%s arg %s was removed',
                                $typeName,
                                $fieldName,
                                $oldArgDef->name
                            ),
                        ];
                    }
                    // Check if a non-null arg was added to the field
                    foreach ($newTypeFields[$fieldName]->args as $newTypeFieldArgDef) {
                        $oldArgs   = $oldTypeFields[$fieldName]->args;
                        $oldArgDef = Utils::find(
                            $oldArgs,
                            static function ($arg) use ($newTypeFieldArgDef) {
                                return $arg->name === $newTypeFieldArgDef->name;
                            }
                        );

                        if ($oldArgDef !== null) {
                            continue;
                        }

                        $newTypeName = $newType->name;
                        $newArgName  = $newTypeFieldArgDef->name;
                        if ($newTypeFieldArgDef->getType() instanceof NonNull) {
                            $breakingChanges[] = [
                                'type'        => self::BREAKING_CHANGE_NON_NULL_ARG_ADDED,
                                'description' => "A non-null arg ${newArgName} on ${newTypeName}.${fieldName} was added",
                            ];
                        } else {
                            $dangerousChanges[] = [
                                'type'        => self::DANGEROUS_CHANGE_NULLABLE_ARG_ADDED,
                                'description' => "A nullable arg ${newArgName} on ${newTypeName}.${fieldName} was added",
                            ];
                        }
                    }
                }
            }
        }

        return [
            'breakingChanges'  => $breakingChanges,
            'dangerousChanges' => $dangerousChanges,
        ];
    }

    /**
     * @return string[][]
     */
    public static function findInterfacesRemovedFromObjectTypes(
        Schema $oldSchema,
        Schema $newSchema
    ) {
        $oldTypeMap      = $oldSchema->getTypeMap();
        $newTypeMap      = $newSchema->getTypeMap();
        $breakingChanges = [];

        foreach ($oldTypeMap as $typeName => $oldType) {
            $newType = $newTypeMap[$typeName] ?? null;
            if (! ($oldType instanceof ObjectType) || ! ($newType instanceof ObjectType)) {
                continue;
            }

            $oldInterfaces = $oldType->getInterfaces();
            $newInterfaces = $newType->getInterfaces();
            foreach ($oldInterfaces as $oldInterface) {
                $interface = Utils::find(
                    $newInterfaces,
                    static function (InterfaceType $interface) use ($oldInterface) : bool {
                        return $interface->name === $oldInterface->name;
                    }
                );
                if ($interface !== null) {
                    continue;
                }

                $breakingChanges[] = [
                    'type'        => self::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT,
                    'description' => sprintf('%s no longer implements interface %s.', $typeName, $oldInterface->name),
                ];
            }
        }

        return $breakingChanges;
    }

    /**
     * @return string[][]
     */
    public static function findRemovedDirectives(Schema $oldSchema, Schema $newSchema)
    {
        $removedDirectives = [];

        $newSchemaDirectiveMap = self::getDirectiveMapForSchema($newSchema);
        foreach ($oldSchema->getDirectives() as $directive) {
            if (isset($newSchemaDirectiveMap[$directive->name])) {
                continue;
            }

            $removedDirectives[] = [
                'type'        => self::BREAKING_CHANGE_DIRECTIVE_REMOVED,
                'description' => sprintf('%s was removed', $directive->name),
            ];
        }

        return $removedDirectives;
    }

    private static function getDirectiveMapForSchema(Schema $schema)
    {
        return Utils::keyMap(
            $schema->getDirectives(),
            static function ($dir) {
                return $dir->name;
            }
        );
    }

    public static function findRemovedDirectiveArgs(Schema $oldSchema, Schema $newSchema)
    {
        $removedDirectiveArgs  = [];
        $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);

        foreach ($newSchema->getDirectives() as $newDirective) {
            if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
                continue;
            }

            foreach (self::findRemovedArgsForDirectives(
                $oldSchemaDirectiveMap[$newDirective->name],
                $newDirective
            ) as $arg) {
                $removedDirectiveArgs[] = [
                    'type'        => self::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED,
                    'description' => sprintf('%s was removed from %s', $arg->name, $newDirective->name),
                ];
            }
        }

        return $removedDirectiveArgs;
    }

    public static function findRemovedArgsForDirectives(Directive $oldDirective, Directive $newDirective)
    {
        $removedArgs = [];
        $newArgMap   = self::getArgumentMapForDirective($newDirective);
        foreach ($oldDirective->args as $arg) {
            if (isset($newArgMap[$arg->name])) {
                continue;
            }

            $removedArgs[] = $arg;
        }

        return $removedArgs;
    }

    private static function getArgumentMapForDirective(Directive $directive)
    {
        return Utils::keyMap(
            $directive->args ?: [],
            static function ($arg) {
                return $arg->name;
            }
        );
    }

    public static function findAddedNonNullDirectiveArgs(Schema $oldSchema, Schema $newSchema)
    {
        $addedNonNullableArgs  = [];
        $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);

        foreach ($newSchema->getDirectives() as $newDirective) {
            if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
                continue;
            }

            foreach (self::findAddedArgsForDirective(
                $oldSchemaDirectiveMap[$newDirective->name],
                $newDirective
            ) as $arg) {
                if (! $arg->getType() instanceof NonNull) {
                    continue;
                }
                $addedNonNullableArgs[] = [
                    'type'        => self::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED,
                    'description' => sprintf(
                        'A non-null arg %s on directive %s was added',
                        $arg->name,
                        $newDirective->name
                    ),
                ];
            }
        }

        return $addedNonNullableArgs;
    }

    /**
     * @return FieldArgument[]
     */
    public static function findAddedArgsForDirective(Directive $oldDirective, Directive $newDirective)
    {
        $addedArgs = [];
        $oldArgMap = self::getArgumentMapForDirective($oldDirective);
        foreach ($newDirective->args as $arg) {
            if (isset($oldArgMap[$arg->name])) {
                continue;
            }

            $addedArgs[] = $arg;
        }

        return $addedArgs;
    }

    /**
     * @return string[][]
     */
    public static function findRemovedDirectiveLocations(Schema $oldSchema, Schema $newSchema)
    {
        $removedLocations      = [];
        $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);

        foreach ($newSchema->getDirectives() as $newDirective) {
            if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
                continue;
            }

            foreach (self::findRemovedLocationsForDirective(
                $oldSchemaDirectiveMap[$newDirective->name],
                $newDirective
            ) as $location) {
                $removedLocations[] = [
                    'type'        => self::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED,
                    'description' => sprintf('%s was removed from %s', $location, $newDirective->name),
                ];
            }
        }

        return $removedLocations;
    }

    public static function findRemovedLocationsForDirective(Directive $oldDirective, Directive $newDirective)
    {
        $removedLocations = [];
        $newLocationSet   = array_flip($newDirective->locations);
        foreach ($oldDirective->locations as $oldLocation) {
            if (array_key_exists($oldLocation, $newLocationSet)) {
                continue;
            }

            $removedLocations[] = $oldLocation;
        }

        return $removedLocations;
    }

    /**
     * Given two schemas, returns an Array containing descriptions of all the types
     * of potentially dangerous changes covered by the other functions down below.
     *
     * @return string[][]
     */
    public static function findDangerousChanges(Schema $oldSchema, Schema $newSchema)
    {
        return array_merge(
            self::findArgChanges($oldSchema, $newSchema)['dangerousChanges'],
            self::findValuesAddedToEnums($oldSchema, $newSchema),
            self::findInterfacesAddedToObjectTypes($oldSchema, $newSchema),
            self::findTypesAddedToUnions($oldSchema, $newSchema),
            self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['dangerousChanges']
        );
    }

    /**
     * Given two schemas, returns an Array containing descriptions of any dangerous
     * changes in the newSchema related to adding values to an enum type.
     *
     * @return string[][]
     */
    public static function findValuesAddedToEnums(
        Schema $oldSchema,
        Schema $newSchema
    ) {
        $oldTypeMap = $oldSchema->getTypeMap();
        $newTypeMap = $newSchema->getTypeMap();

        $valuesAddedToEnums = [];
        foreach ($oldTypeMap as $typeName => $oldType) {
            $newType = $newTypeMap[$typeName] ?? null;
            if (! ($oldType instanceof EnumType) || ! ($newType instanceof EnumType)) {
                continue;
            }
            $valuesInOldEnum = [];
            foreach ($oldType->getValues() as $value) {
                $valuesInOldEnum[$value->name] = true;
            }
            foreach ($newType->getValues() as $value) {
                if (isset($valuesInOldEnum[$value->name])) {
                    continue;
                }

                $valuesAddedToEnums[] = [
                    'type'        => self::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM,
                    'description' => sprintf('%s was added to enum type %s.', $value->name, $typeName),
                ];
            }
        }

        return $valuesAddedToEnums;
    }

    /**
     * @return string[][]
     */
    public static function findInterfacesAddedToObjectTypes(
        Schema $oldSchema,
        Schema $newSchema
    ) {
        $oldTypeMap                   = $oldSchema->getTypeMap();
        $newTypeMap                   = $newSchema->getTypeMap();
        $interfacesAddedToObjectTypes = [];

        foreach ($newTypeMap as $typeName => $newType) {
            $oldType = $oldTypeMap[$typeName] ?? null;
            if (! ($oldType instanceof ObjectType) || ! ($newType instanceof ObjectType)) {
                continue;
            }

            $oldInterfaces = $oldType->getInterfaces();
            $newInterfaces = $newType->getInterfaces();
            foreach ($newInterfaces as $newInterface) {
                $interface = Utils::find(
                    $oldInterfaces,
                    static function (InterfaceType $interface) use ($newInterface) : bool {
                        return $interface->name === $newInterface->name;
                    }
                );

                if ($interface !== null) {
                    continue;
                }

                $interfacesAddedToObjectTypes[] = [
                    'type'        => self::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT,
                    'description' => sprintf(
                        '%s added to interfaces implemented by %s.',
                        $newInterface->name,
                        $typeName
                    ),
                ];
            }
        }

        return $interfacesAddedToObjectTypes;
    }

    /**
     * Given two schemas, returns an Array containing descriptions of any dangerous
     * changes in the newSchema related to adding types to a union type.
     *
     * @return string[][]
     */
    public static function findTypesAddedToUnions(
        Schema $oldSchema,
        Schema $newSchema
    ) {
        $oldTypeMap = $oldSchema->getTypeMap();
        $newTypeMap = $newSchema->getTypeMap();

        $typesAddedToUnion = [];
        foreach ($newTypeMap as $typeName => $newType) {
            $oldType = $oldTypeMap[$typeName] ?? null;
            if (! ($oldType instanceof UnionType) || ! ($newType instanceof UnionType)) {
                continue;
            }

            $typeNamesInOldUnion = [];
            foreach ($oldType->getTypes() as $type) {
                $typeNamesInOldUnion[$type->name] = true;
            }
            foreach ($newType->getTypes() as $type) {
                if (isset($typeNamesInOldUnion[$type->name])) {
                    continue;
                }

                $typesAddedToUnion[] = [
                    'type'        => self::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION,
                    'description' => sprintf('%s was added to union type %s.', $type->name, $typeName),
                ];
            }
        }

        return $typesAddedToUnion;
    }
}

class_alias(BreakingChangesFinder::class, 'GraphQL\Utils\FindBreakingChanges');


<?php

declare(strict_types=1);

namespace GraphQL\Type;

use Exception;
use GraphQL\Language\DirectiveLocation;
use GraphQL\Language\Printer;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputObjectField;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Definition\WrappingType;
use GraphQL\Utils\AST;
use GraphQL\Utils\Utils;
use function array_filter;
use function array_key_exists;
use function array_values;
use function in_array;
use function is_bool;
use function method_exists;
use function trigger_error;
use const E_USER_DEPRECATED;

class Introspection
{
    const SCHEMA_FIELD_NAME    = '__schema';
    const TYPE_FIELD_NAME      = '__type';
    const TYPE_NAME_FIELD_NAME = '__typename';

    /** @var Type[] */
    private static $map = [];

    /**
     * Options:
     *   - descriptions
     *     Whether to include descriptions in the introspection result.
     *     Default: true
     *
     * @param bool[]|bool $options
     *
     * @return string
     */
    public static function getIntrospectionQuery($options = [])
    {
        if (is_bool($options)) {
            trigger_error(
                'Calling Introspection::getIntrospectionQuery(boolean) is deprecated. ' .
                'Please use Introspection::getIntrospectionQuery(["descriptions" => boolean]).',
                E_USER_DEPRECATED
            );
            $descriptions = $options;
        } else {
            $descriptions = ! array_key_exists('descriptions', $options) || $options['descriptions'] === true;
        }
        $descriptionField = $descriptions ? 'description' : '';

        return <<<EOD
  query IntrospectionQuery {
    __schema {
      queryType { name }
      mutationType { name }
      subscriptionType { name }
      types {
        ...FullType
      }
      directives {
        name
        {$descriptionField}
        locations
        args {
          ...InputValue
        }
      }
    }
  }

  fragment FullType on __Type {
    kind
    name
    {$descriptionField}
    fields(includeDeprecated: true) {
      name
      {$descriptionField}
      args {
        ...InputValue
      }
      type {
        ...TypeRef
      }
      isDeprecated
      deprecationReason
    }
    inputFields {
      ...InputValue
    }
    interfaces {
      ...TypeRef
    }
    enumValues(includeDeprecated: true) {
      name
      {$descriptionField}
      isDeprecated
      deprecationReason
    }
    possibleTypes {
      ...TypeRef
    }
  }

  fragment InputValue on __InputValue {
    name
    {$descriptionField}
    type { ...TypeRef }
    defaultValue
  }

  fragment TypeRef on __Type {
    kind
    name
    ofType {
      kind
      name
      ofType {
        kind
        name
        ofType {
          kind
          name
          ofType {
            kind
            name
            ofType {
              kind
              name
              ofType {
                kind
                name
                ofType {
                  kind
                  name
                }
              }
            }
          }
        }
      }
    }
  }
EOD;
    }

    /**
     * @param Type $type
     *
     * @return bool
     */
    public static function isIntrospectionType($type)
    {
        return array_key_exists($type->name, self::getTypes());
    }

    public static function getTypes()
    {
        return [
            '__Schema'            => self::_schema(),
            '__Type'              => self::_type(),
            '__Directive'         => self::_directive(),
            '__Field'             => self::_field(),
            '__InputValue'        => self::_inputValue(),
            '__EnumValue'         => self::_enumValue(),
            '__TypeKind'          => self::_typeKind(),
            '__DirectiveLocation' => self::_directiveLocation(),
        ];
    }

    public static function _schema()
    {
        if (! isset(self::$map['__Schema'])) {
            self::$map['__Schema'] = new ObjectType([
                'name'            => '__Schema',
                'isIntrospection' => true,
                'description'     =>
                    'A GraphQL Schema defines the capabilities of a GraphQL ' .
                    'server. It exposes all available types and directives on ' .
                    'the server, as well as the entry points for query, mutation, and ' .
                    'subscription operations.',
                'fields'          => [
                    'types'            => [
                        'description' => 'A list of all types supported by this server.',
                        'type'        => new NonNull(new ListOfType(new NonNull(self::_type()))),
                        'resolve'     => static function (Schema $schema) {
                            return array_values($schema->getTypeMap());
                        },
                    ],
                    'queryType'        => [
                        'description' => 'The type that query operations will be rooted at.',
                        'type'        => new NonNull(self::_type()),
                        'resolve'     => static function (Schema $schema) {
                            return $schema->getQueryType();
                        },
                    ],
                    'mutationType'     => [
                        'description' =>
                            'If this server supports mutation, the type that ' .
                            'mutation operations will be rooted at.',
                        'type'        => self::_type(),
                        'resolve'     => static function (Schema $schema) {
                            return $schema->getMutationType();
                        },
                    ],
                    'subscriptionType' => [
                        'description' => 'If this server support subscription, the type that subscription operations will be rooted at.',
                        'type'        => self::_type(),
                        'resolve'     => static function (Schema $schema) {
                            return $schema->getSubscriptionType();
                        },
                    ],
                    'directives'       => [
                        'description' => 'A list of all directives supported by this server.',
                        'type'        => Type::nonNull(Type::listOf(Type::nonNull(self::_directive()))),
                        'resolve'     => static function (Schema $schema) {
                            return $schema->getDirectives();
                        },
                    ],
                ],
            ]);
        }

        return self::$map['__Schema'];
    }

    public static function _type()
    {
        if (! isset(self::$map['__Type'])) {
            self::$map['__Type'] = new ObjectType([
                'name'            => '__Type',
                'isIntrospection' => true,
                'description'     =>
                    'The fundamental unit of any GraphQL Schema is the type. There are ' .
                    'many kinds of types in GraphQL as represented by the `__TypeKind` enum.' .
                    "\n\n" .
                    'Depending on the kind of a type, certain fields describe ' .
                    'information about that type. Scalar types provide no information ' .
                    'beyond a name and description, while Enum types provide their values. ' .
                    'Object and Interface types provide the fields they describe. Abstract ' .
                    'types, Union and Interface, provide the Object types possible ' .
                    'at runtime. List and NonNull types compose other types.',
                'fields'          => static function () {
                    return [
                        'kind'          => [
                            'type'    => Type::nonNull(self::_typeKind()),
                            'resolve' => static function (Type $type) {
                                switch (true) {
                                    case $type instanceof ListOfType:
                                        return TypeKind::LIST_KIND;
                                    case $type instanceof NonNull:
                                        return TypeKind::NON_NULL;
                                    case $type instanceof ScalarType:
                                        return TypeKind::SCALAR;
                                    case $type instanceof ObjectType:
                                        return TypeKind::OBJECT;
                                    case $type instanceof EnumType:
                                        return TypeKind::ENUM;
                                    case $type instanceof InputObjectType:
                                        return TypeKind::INPUT_OBJECT;
                                    case $type instanceof InterfaceType:
                                        return TypeKind::INTERFACE_KIND;
                                    case $type instanceof UnionType:
                                        return TypeKind::UNION;
                                    default:
                                        throw new Exception('Unknown kind of type: ' . Utils::printSafe($type));
                                }
                            },
                        ],
                        'name'          => ['type' => Type::string()],
                        'description'   => ['type' => Type::string()],
                        'fields'        => [
                            'type'    => Type::listOf(Type::nonNull(self::_field())),
                            'args'    => [
                                'includeDeprecated' => ['type' => Type::boolean(), 'defaultValue' => false],
                            ],
                            'resolve' => static function (Type $type, $args) {
                                if ($type instanceof ObjectType || $type instanceof InterfaceType) {
                                    $fields = $type->getFields();

                                    if (empty($args['includeDeprecated'])) {
                                        $fields = array_filter(
                                            $fields,
                                            static function (FieldDefinition $field) {
                                                return ! $field->deprecationReason;
                                            }
                                        );
                                    }

                                    return array_values($fields);
                                }

                                return null;
                            },
                        ],
                        'interfaces'    => [
                            'type'    => Type::listOf(Type::nonNull(self::_type())),
                            'resolve' => static function ($type) {
                                if ($type instanceof ObjectType) {
                                    return $type->getInterfaces();
                                }

                                return null;
                            },
                        ],
                        'possibleTypes' => [
                            'type'    => Type::listOf(Type::nonNull(self::_type())),
                            'resolve' => static function ($type, $args, $context, ResolveInfo $info) {
                                if ($type instanceof InterfaceType || $type instanceof UnionType) {
                                    return $info->schema->getPossibleTypes($type);
                                }

                                return null;
                            },
                        ],
                        'enumValues'    => [
                            'type'    => Type::listOf(Type::nonNull(self::_enumValue())),
                            'args'    => [
                                'includeDeprecated' => ['type' => Type::boolean(), 'defaultValue' => false],
                            ],
                            'resolve' => static function ($type, $args) {
                                if ($type instanceof EnumType) {
                                    $values = array_values($type->getValues());

                                    if (empty($args['includeDeprecated'])) {
                                        $values = array_filter(
                                            $values,
                                            static function ($value) {
                                                return ! $value->deprecationReason;
                                            }
                                        );
                                    }

                                    return $values;
                                }

                                return null;
                            },
                        ],
                        'inputFields'   => [
                            'type'    => Type::listOf(Type::nonNull(self::_inputValue())),
                            'resolve' => static function ($type) {
                                if ($type instanceof InputObjectType) {
                                    return array_values($type->getFields());
                                }

                                return null;
                            },
                        ],
                        'ofType'        => [
                            'type'    => self::_type(),
                            'resolve' => static function ($type) {
                                if ($type instanceof WrappingType) {
                                    return $type->getWrappedType();
                                }

                                return null;
                            },
                        ],
                    ];
                },
            ]);
        }

        return self::$map['__Type'];
    }

    public static function _typeKind()
    {
        if (! isset(self::$map['__TypeKind'])) {
            self::$map['__TypeKind'] = new EnumType([
                'name'            => '__TypeKind',
                'isIntrospection' => true,
                'description'     => 'An enum describing what kind of type a given `__Type` is.',
                'values'          => [
                    'SCALAR'       => [
                        'value'       => TypeKind::SCALAR,
                        'description' => 'Indicates this type is a scalar.',
                    ],
                    'OBJECT'       => [
                        'value'       => TypeKind::OBJECT,
                        'description' => 'Indicates this type is an object. `fields` and `interfaces` are valid fields.',
                    ],
                    'INTERFACE'    => [
                        'value'       => TypeKind::INTERFACE_KIND,
                        'description' => 'Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.',
                    ],
                    'UNION'        => [
                        'value'       => TypeKind::UNION,
                        'description' => 'Indicates this type is a union. `possibleTypes` is a valid field.',
                    ],
                    'ENUM'         => [
                        'value'       => TypeKind::ENUM,
                        'description' => 'Indicates this type is an enum. `enumValues` is a valid field.',
                    ],
                    'INPUT_OBJECT' => [
                        'value'       => TypeKind::INPUT_OBJECT,
                        'description' => 'Indicates this type is an input object. `inputFields` is a valid field.',
                    ],
                    'LIST'         => [
                        'value'       => TypeKind::LIST_KIND,
                        'description' => 'Indicates this type is a list. `ofType` is a valid field.',
                    ],
                    'NON_NULL'     => [
                        'value'       => TypeKind::NON_NULL,
                        'description' => 'Indicates this type is a non-null. `ofType` is a valid field.',
                    ],
                ],
            ]);
        }

        return self::$map['__TypeKind'];
    }

    public static function _field()
    {
        if (! isset(self::$map['__Field'])) {
            self::$map['__Field'] = new ObjectType([
                'name'            => '__Field',
                'isIntrospection' => true,
                'description'     =>
                    'Object and Interface types are described by a list of Fields, each of ' .
                    'which has a name, potentially a list of arguments, and a return type.',
                'fields'          => static function () {
                    return [
                        'name'              => ['type' => Type::nonNull(Type::string())],
                        'description'       => ['type' => Type::string()],
                        'args'              => [
                            'type'    => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))),
                            'resolve' => static function (FieldDefinition $field) {
                                return empty($field->args) ? [] : $field->args;
                            },
                        ],
                        'type'              => [
                            'type'    => Type::nonNull(self::_type()),
                            'resolve' => static function (FieldDefinition $field) {
                                return $field->getType();
                            },
                        ],
                        'isDeprecated'      => [
                            'type'    => Type::nonNull(Type::boolean()),
                            'resolve' => static function (FieldDefinition $field) {
                                return (bool) $field->deprecationReason;
                            },
                        ],
                        'deprecationReason' => [
                            'type' => Type::string(),
                        ],
                    ];
                },
            ]);
        }

        return self::$map['__Field'];
    }

    public static function _inputValue()
    {
        if (! isset(self::$map['__InputValue'])) {
            self::$map['__InputValue'] = new ObjectType([
                'name'            => '__InputValue',
                'isIntrospection' => true,
                'description'     =>
                    'Arguments provided to Fields or Directives and the input fields of an ' .
                    'InputObject are represented as Input Values which describe their type ' .
                    'and optionally a default value.',
                'fields'          => static function () {
                    return [
                        'name'         => ['type' => Type::nonNull(Type::string())],
                        'description'  => ['type' => Type::string()],
                        'type'         => [
                            'type'    => Type::nonNull(self::_type()),
                            'resolve' => static function ($value) {
                                return method_exists($value, 'getType') ? $value->getType() : $value->type;
                            },
                        ],
                        'defaultValue' => [
                            'type'        => Type::string(),
                            'description' =>
                                'A GraphQL-formatted string representing the default value for this input value.',
                            'resolve'     => static function ($inputValue) {
                                /** @var FieldArgument|InputObjectField $inputValue */
                                return ! $inputValue->defaultValueExists()
                                    ? null
                                    : Printer::doPrint(AST::astFromValue(
                                        $inputValue->defaultValue,
                                        $inputValue->getType()
                                    ));
                            },
                        ],
                    ];
                },
            ]);
        }

        return self::$map['__InputValue'];
    }

    public static function _enumValue()
    {
        if (! isset(self::$map['__EnumValue'])) {
            self::$map['__EnumValue'] = new ObjectType([
                'name'            => '__EnumValue',
                'isIntrospection' => true,
                'description'     =>
                    'One possible value for a given Enum. Enum values are unique values, not ' .
                    'a placeholder for a string or numeric value. However an Enum value is ' .
                    'returned in a JSON response as a string.',
                'fields'          => [
                    'name'              => ['type' => Type::nonNull(Type::string())],
                    'description'       => ['type' => Type::string()],
                    'isDeprecated'      => [
                        'type'    => Type::nonNull(Type::boolean()),
                        'resolve' => static function ($enumValue) {
                            return (bool) $enumValue->deprecationReason;
                        },
                    ],
                    'deprecationReason' => [
                        'type' => Type::string(),
                    ],
                ],
            ]);
        }

        return self::$map['__EnumValue'];
    }

    public static function _directive()
    {
        if (! isset(self::$map['__Directive'])) {
            self::$map['__Directive'] = new ObjectType([
                'name'            => '__Directive',
                'isIntrospection' => true,
                'description'     => 'A Directive provides a way to describe alternate runtime execution and ' .
                    'type validation behavior in a GraphQL document.' .
                    "\n\nIn some cases, you need to provide options to alter GraphQL's " .
                    'execution behavior in ways field arguments will not suffice, such as ' .
                    'conditionally including or skipping a field. Directives provide this by ' .
                    'describing additional information to the executor.',
                'fields'          => [
                    'name'        => ['type' => Type::nonNull(Type::string())],
                    'description' => ['type' => Type::string()],
                    'locations'   => [
                        'type' => Type::nonNull(Type::listOf(Type::nonNull(
                            self::_directiveLocation()
                        ))),
                    ],
                    'args'        => [
                        'type'    => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))),
                        'resolve' => static function (Directive $directive) {
                            return $directive->args ?: [];
                        },
                    ],

                    // NOTE: the following three fields are deprecated and are no longer part
                    // of the GraphQL specification.
                    'onOperation' => [
                        'deprecationReason' => 'Use `locations`.',
                        'type'              => Type::nonNull(Type::boolean()),
                        'resolve'           => static function ($d) {
                            return in_array(DirectiveLocation::QUERY, $d->locations, true) ||
                                in_array(DirectiveLocation::MUTATION, $d->locations, true) ||
                                in_array(DirectiveLocation::SUBSCRIPTION, $d->locations, true);
                        },
                    ],
                    'onFragment'  => [
                        'deprecationReason' => 'Use `locations`.',
                        'type'              => Type::nonNull(Type::boolean()),
                        'resolve'           => static function ($d) {
                            return in_array(DirectiveLocation::FRAGMENT_SPREAD, $d->locations, true) ||
                                in_array(DirectiveLocation::INLINE_FRAGMENT, $d->locations, true) ||
                                in_array(DirectiveLocation::FRAGMENT_DEFINITION, $d->locations, true);
                        },
                    ],
                    'onField'     => [
                        'deprecationReason' => 'Use `locations`.',
                        'type'              => Type::nonNull(Type::boolean()),
                        'resolve'           => static function ($d) {
                            return in_array(DirectiveLocation::FIELD, $d->locations, true);
                        },
                    ],
                ],
            ]);
        }

        return self::$map['__Directive'];
    }

    public static function _directiveLocation()
    {
        if (! isset(self::$map['__DirectiveLocation'])) {
            self::$map['__DirectiveLocation'] = new EnumType([
                'name'            => '__DirectiveLocation',
                'isIntrospection' => true,
                'description'     =>
                    'A Directive can be adjacent to many parts of the GraphQL language, a ' .
                    '__DirectiveLocation describes one such possible adjacencies.',
                'values'          => [
                    'QUERY'                  => [
                        'value'       => DirectiveLocation::QUERY,
                        'description' => 'Location adjacent to a query operation.',
                    ],
                    'MUTATION'               => [
                        'value'       => DirectiveLocation::MUTATION,
                        'description' => 'Location adjacent to a mutation operation.',
                    ],
                    'SUBSCRIPTION'           => [
                        'value'       => DirectiveLocation::SUBSCRIPTION,
                        'description' => 'Location adjacent to a subscription operation.',
                    ],
                    'FIELD'                  => [
                        'value'       => DirectiveLocation::FIELD,
                        'description' => 'Location adjacent to a field.',
                    ],
                    'FRAGMENT_DEFINITION'    => [
                        'value'       => DirectiveLocation::FRAGMENT_DEFINITION,
                        'description' => 'Location adjacent to a fragment definition.',
                    ],
                    'FRAGMENT_SPREAD'        => [
                        'value'       => DirectiveLocation::FRAGMENT_SPREAD,
                        'description' => 'Location adjacent to a fragment spread.',
                    ],
                    'INLINE_FRAGMENT'        => [
                        'value'       => DirectiveLocation::INLINE_FRAGMENT,
                        'description' => 'Location adjacent to an inline fragment.',
                    ],
                    'SCHEMA'                 => [
                        'value'       => DirectiveLocation::SCHEMA,
                        'description' => 'Location adjacent to a schema definition.',
                    ],
                    'SCALAR'                 => [
                        'value'       => DirectiveLocation::SCALAR,
                        'description' => 'Location adjacent to a scalar definition.',
                    ],
                    'OBJECT'                 => [
                        'value'       => DirectiveLocation::OBJECT,
                        'description' => 'Location adjacent to an object type definition.',
                    ],
                    'FIELD_DEFINITION'       => [
                        'value'       => DirectiveLocation::FIELD_DEFINITION,
                        'description' => 'Location adjacent to a field definition.',
                    ],
                    'ARGUMENT_DEFINITION'    => [
                        'value'       => DirectiveLocation::ARGUMENT_DEFINITION,
                        'description' => 'Location adjacent to an argument definition.',
                    ],
                    'INTERFACE'              => [
                        'value'       => DirectiveLocation::IFACE,
                        'description' => 'Location adjacent to an interface definition.',
                    ],
                    'UNION'                  => [
                        'value'       => DirectiveLocation::UNION,
                        'description' => 'Location adjacent to a union definition.',
                    ],
                    'ENUM'                   => [
                        'value'       => DirectiveLocation::ENUM,
                        'description' => 'Location adjacent to an enum definition.',
                    ],
                    'ENUM_VALUE'             => [
                        'value'       => DirectiveLocation::ENUM_VALUE,
                        'description' => 'Location adjacent to an enum value definition.',
                    ],
                    'INPUT_OBJECT'           => [
                        'value'       => DirectiveLocation::INPUT_OBJECT,
                        'description' => 'Location adjacent to an input object type definition.',
                    ],
                    'INPUT_FIELD_DEFINITION' => [
                        'value'       => DirectiveLocation::INPUT_FIELD_DEFINITION,
                        'description' => 'Location adjacent to an input object field definition.',
                    ],

                ],
            ]);
        }

        return self::$map['__DirectiveLocation'];
    }

    public static function schemaMetaFieldDef()
    {
        if (! isset(self::$map[self::SCHEMA_FIELD_NAME])) {
            self::$map[self::SCHEMA_FIELD_NAME] = FieldDefinition::create([
                'name'        => self::SCHEMA_FIELD_NAME,
                'type'        => Type::nonNull(self::_schema()),
                'description' => 'Access the current type schema of this server.',
                'args'        => [],
                'resolve'     => static function (
                    $source,
                    $args,
                    $context,
                    ResolveInfo $info
                ) {
                    return $info->schema;
                },
            ]);
        }

        return self::$map[self::SCHEMA_FIELD_NAME];
    }

    public static function typeMetaFieldDef()
    {
        if (! isset(self::$map[self::TYPE_FIELD_NAME])) {
            self::$map[self::TYPE_FIELD_NAME] = FieldDefinition::create([
                'name'        => self::TYPE_FIELD_NAME,
                'type'        => self::_type(),
                'description' => 'Request the type information of a single type.',
                'args'        => [
                    ['name' => 'name', 'type' => Type::nonNull(Type::string())],
                ],
                'resolve'     => static function ($source, $args, $context, ResolveInfo $info) {
                    return $info->schema->getType($args['name']);
                },
            ]);
        }

        return self::$map[self::TYPE_FIELD_NAME];
    }

    public static function typeNameMetaFieldDef()
    {
        if (! isset(self::$map[self::TYPE_NAME_FIELD_NAME])) {
            self::$map[self::TYPE_NAME_FIELD_NAME] = FieldDefinition::create([
                'name'        => self::TYPE_NAME_FIELD_NAME,
                'type'        => Type::nonNull(Type::string()),
                'description' => 'The name of the current Object type at runtime.',
                'args'        => [],
                'resolve'     => static function (
                    $source,
                    $args,
                    $context,
                    ResolveInfo $info
                ) {
                    return $info->parentType->name;
                },
            ]);
        }

        return self::$map[self::TYPE_NAME_FIELD_NAME];
    }
}