Completed
Pull Request — master (#45)
by Christoffer
02:03
created

getSuggestedFieldNames()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 2
nop 2
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Digia\GraphQL\Validation\Rule;
4
5
use Digia\GraphQL\Error\ValidationException;
6
use Digia\GraphQL\Language\AST\Node\FieldNode;
7
use Digia\GraphQL\Language\AST\Node\InterfacesTrait;
8
use Digia\GraphQL\Language\AST\Node\NodeInterface;
9
use Digia\GraphQL\Type\Definition\AbstractTypeInterface;
10
use Digia\GraphQL\Type\Definition\FieldsTrait;
11
use Digia\GraphQL\Type\Definition\InterfaceType;
12
use Digia\GraphQL\Type\Definition\NameTrait;
13
use Digia\GraphQL\Type\Definition\ObjectType;
14
use Digia\GraphQL\Type\Definition\OutputTypeInterface;
15
use Digia\GraphQL\Type\Definition\TypeInterface;
16
use Digia\GraphQL\Type\SchemaInterface;
17
use function Digia\GraphQL\Util\suggestionList;
18
19
/**
20
 * Fields on correct type
21
 *
22
 * A GraphQL document is only valid if all fields selected are defined by the
23
 * parent type, or are an allowed meta field such as __typename.
24
 */
25
class FieldOnCorrectTypeRule extends AbstractRule
26
{
27
    /**
28
     * @inheritdoc
29
     */
30
    public function enterNode(NodeInterface $node): ?NodeInterface
31
    {
32
        if ($node instanceof FieldNode) {
33
            $type = $this->context->getParentType();
34
35
            if (null !== $type && $type instanceof OutputTypeInterface) {
36
                $fieldDefinition = $this->context->getFieldDefinition();
37
38
                if (null === $fieldDefinition) {
39
                    $schema              = $this->context->getSchema();
40
                    $fieldName           = $node->getNameValue();
41
                    $suggestedTypeNames  = getSuggestedTypeNames($schema, $type, $fieldName);
42
                    $suggestedFieldNames = count($suggestedTypeNames) !== 0
43
                        ? []
44
                        : getSuggestedFieldNames($type, $fieldName);
45
46
                    $this->context->reportError(
47
                        new ValidationException(
48
                            undefinedFieldMessage(
49
                                $fieldName,
50
                                (string)$type,
51
                                $suggestedTypeNames,
52
                                $suggestedFieldNames
53
                            ),
54
                            [$node]
55
                        )
56
                    );
57
                }
58
            }
59
        }
60
61
        return $node;
62
    }
63
}
64
65
/**
66
 * Go through all of the implementations of type, as well as the interfaces
67
 * that they implement. If any of those types include the provided field,
68
 * suggest them, sorted by how often the type is referenced,  starting
69
 * with Interfaces.
70
 *
71
 * @param SchemaInterface $schema
72
 * @param TypeInterface   $type
73
 * @param string          $fieldName
74
 * @return array
75
 * @throws \Exception
76
 */
77
function getSuggestedTypeNames(SchemaInterface $schema, TypeInterface $type, string $fieldName): array
78
{
79
    if (!$type instanceof AbstractTypeInterface) {
80
        // Otherwise, must be an Object type, which does not have possible fields.
81
        return [];
82
    }
83
84
    $suggestedObjectTypes = [];
85
    $interfaceUsageCount  = [];
86
87
    /** @var FieldsTrait|NameTrait|InterfacesTrait $possibleType */
88
    foreach ($schema->getPossibleTypes($type) as $possibleType) {
89
        $typeFields = $possibleType->getFields();
90
        if (!isset($typeFields[$fieldName])) {
91
            break;
92
        }
93
94
        $suggestedObjectTypes[] = $possibleType->getName();
95
96
        /** @var InterfaceType $possibleInterface */
97
        foreach ($possibleType->getInterfaces() as $possibleInterface) {
98
            $interfaceFields = $possibleInterface->getFields();
99
            if (!isset($interfaceFields[$fieldName])) {
100
                break;
101
            }
102
103
            $interfaceName                       = $possibleInterface->getName();
104
            $interfaceUsageCount[$interfaceName] = ($interfaceUsageCount[$interfaceName] ?? 0) + 1;
105
        }
106
    }
107
108
    $suggestedInterfaceTypes = array_keys($interfaceUsageCount);
109
110
    uasort($suggestedInterfaceTypes, function ($a, $b) use ($interfaceUsageCount) {
111
        return $interfaceUsageCount[$b] - $interfaceUsageCount[$a];
112
    });
113
114
    return array_merge($suggestedInterfaceTypes, $suggestedObjectTypes);
115
}
116
117
/**
118
 * For the field name provided, determine if there are any similar field names
119
 * that may be the result of a typo.
120
 *
121
 * @param OutputTypeInterface $type
122
 * @param string              $fieldName
123
 * @return array
124
 * @throws \Exception
125
 */
126
function getSuggestedFieldNames(OutputTypeInterface $type, string $fieldName): array
127
{
128
    if (!($type instanceof ObjectType || $type instanceof InterfaceType)) {
129
        // Otherwise, must be a Union type, which does not define fields.
130
        return [];
131
    }
132
133
    /** @var FieldsTrait $type */
134
    $possibleFieldNames = array_keys($type->getFields());
135
    return suggestionList($fieldName, $possibleFieldNames);
136
}
137