GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

isChangeSafeForObjectOrInterfaceField()   B
last analyzed

Complexity

Conditions 11
Paths 13

Size

Total Lines 32
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 11.0295

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 18
dl 0
loc 32
ccs 15
cts 16
cp 0.9375
rs 7.3166
c 1
b 0
f 0
cc 11
nc 13
nop 2
crap 11.0295

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Utility for finding breaking/dangerous changes between two schemas.
7
 */
8
9
namespace GraphQL\Utils;
10
11
use GraphQL\Type\Definition\Directive;
12
use GraphQL\Type\Definition\EnumType;
13
use GraphQL\Type\Definition\FieldArgument;
14
use GraphQL\Type\Definition\InputObjectType;
15
use GraphQL\Type\Definition\InterfaceType;
16
use GraphQL\Type\Definition\ListOfType;
17
use GraphQL\Type\Definition\NamedType;
18
use GraphQL\Type\Definition\NonNull;
19
use GraphQL\Type\Definition\ObjectType;
20
use GraphQL\Type\Definition\ScalarType;
21
use GraphQL\Type\Definition\Type;
22
use GraphQL\Type\Definition\UnionType;
23
use GraphQL\Type\Schema;
24
use TypeError;
25
use function array_flip;
26
use function array_key_exists;
27
use function array_keys;
28
use function array_merge;
29
use function class_alias;
30
use function sprintf;
31
32
class BreakingChangesFinder
33
{
34
    public const BREAKING_CHANGE_FIELD_CHANGED_KIND            = 'FIELD_CHANGED_KIND';
35
    public const BREAKING_CHANGE_FIELD_REMOVED                 = 'FIELD_REMOVED';
36
    public const BREAKING_CHANGE_TYPE_CHANGED_KIND             = 'TYPE_CHANGED_KIND';
37
    public const BREAKING_CHANGE_TYPE_REMOVED                  = 'TYPE_REMOVED';
38
    public const BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION       = 'TYPE_REMOVED_FROM_UNION';
39
    public const BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM       = 'VALUE_REMOVED_FROM_ENUM';
40
    public const BREAKING_CHANGE_ARG_REMOVED                   = 'ARG_REMOVED';
41
    public const BREAKING_CHANGE_ARG_CHANGED_KIND              = 'ARG_CHANGED_KIND';
42
    public const BREAKING_CHANGE_NON_NULL_ARG_ADDED            = 'NON_NULL_ARG_ADDED';
43
    public const BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED    = 'NON_NULL_INPUT_FIELD_ADDED';
44
    public const BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT = 'INTERFACE_REMOVED_FROM_OBJECT';
45
    public const BREAKING_CHANGE_DIRECTIVE_REMOVED             = 'DIRECTIVE_REMOVED';
46
    public const BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED         = 'DIRECTIVE_ARG_REMOVED';
47
    public const BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED    = 'DIRECTIVE_LOCATION_REMOVED';
48
    public const BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED  = 'NON_NULL_DIRECTIVE_ARG_ADDED';
49
    public const DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED    = 'ARG_DEFAULT_VALUE_CHANGE';
50
    public const DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM          = 'VALUE_ADDED_TO_ENUM';
51
    public const DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT    = 'INTERFACE_ADDED_TO_OBJECT';
52
    public const DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION          = 'TYPE_ADDED_TO_UNION';
53
    public const DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED   = 'NULLABLE_INPUT_FIELD_ADDED';
54
    public const DANGEROUS_CHANGE_NULLABLE_ARG_ADDED           = 'NULLABLE_ARG_ADDED';
55
56
    /**
57
     * Given two schemas, returns an Array containing descriptions of all the types
58
     * of breaking changes covered by the other functions down below.
59
     *
60
     * @return string[][]
61
     */
62 1
    public static function findBreakingChanges(Schema $oldSchema, Schema $newSchema)
63
    {
64 1
        return array_merge(
65 1
            self::findRemovedTypes($oldSchema, $newSchema),
66 1
            self::findTypesThatChangedKind($oldSchema, $newSchema),
67 1
            self::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema),
68 1
            self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['breakingChanges'],
69 1
            self::findTypesRemovedFromUnions($oldSchema, $newSchema),
70 1
            self::findValuesRemovedFromEnums($oldSchema, $newSchema),
71 1
            self::findArgChanges($oldSchema, $newSchema)['breakingChanges'],
72 1
            self::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema),
73 1
            self::findRemovedDirectives($oldSchema, $newSchema),
74 1
            self::findRemovedDirectiveArgs($oldSchema, $newSchema),
75 1
            self::findAddedNonNullDirectiveArgs($oldSchema, $newSchema),
76 1
            self::findRemovedDirectiveLocations($oldSchema, $newSchema)
77
        );
78
    }
79
80
    /**
81
     * Given two schemas, returns an Array containing descriptions of any breaking
82
     * changes in the newSchema related to removing an entire type.
83
     *
84
     * @return string[][]
85
     */
86 2
    public static function findRemovedTypes(
87
        Schema $oldSchema,
88
        Schema $newSchema
89
    ) {
90 2
        $oldTypeMap = $oldSchema->getTypeMap();
91 2
        $newTypeMap = $newSchema->getTypeMap();
92
93 2
        $breakingChanges = [];
94 2
        foreach (array_keys($oldTypeMap) as $typeName) {
95 2
            if (isset($newTypeMap[$typeName])) {
96 2
                continue;
97
            }
98
99 2
            $breakingChanges[] = [
100 2
                'type'        => self::BREAKING_CHANGE_TYPE_REMOVED,
101 2
                'description' => "${typeName} was removed.",
102
            ];
103
        }
104
105 2
        return $breakingChanges;
106
    }
107
108
    /**
109
     * Given two schemas, returns an Array containing descriptions of any breaking
110
     * changes in the newSchema related to changing the type of a type.
111
     *
112
     * @return string[][]
113
     */
114 3
    public static function findTypesThatChangedKind(
115
        Schema $schemaA,
116
        Schema $schemaB
117
    ) : iterable {
118 3
        $schemaATypeMap = $schemaA->getTypeMap();
119 3
        $schemaBTypeMap = $schemaB->getTypeMap();
120
121 3
        $breakingChanges = [];
122 3
        foreach ($schemaATypeMap as $typeName => $schemaAType) {
123 3
            if (! isset($schemaBTypeMap[$typeName])) {
124 1
                continue;
125
            }
126 3
            $schemaBType = $schemaBTypeMap[$typeName];
127 3
            if ($schemaAType instanceof $schemaBType) {
128 3
                continue;
129
            }
130
131 3
            if ($schemaBType instanceof $schemaAType) {
132 1
                continue;
133
            }
134
135 2
            $schemaATypeKindName = self::typeKindName($schemaAType);
136 2
            $schemaBTypeKindName = self::typeKindName($schemaBType);
137 2
            $breakingChanges[]   = [
138 2
                'type'        => self::BREAKING_CHANGE_TYPE_CHANGED_KIND,
139 2
                'description' => "${typeName} changed from ${schemaATypeKindName} to ${schemaBTypeKindName}.",
140
            ];
141
        }
142
143 3
        return $breakingChanges;
144
    }
145
146
    /**
147
     * @return string
148
     *
149
     * @throws TypeError
150
     */
151 2
    private static function typeKindName(Type $type)
152
    {
153 2
        if ($type instanceof ScalarType) {
154
            return 'a Scalar type';
155
        }
156
157 2
        if ($type instanceof ObjectType) {
158 1
            return 'an Object type';
159
        }
160
161 2
        if ($type instanceof InterfaceType) {
162 2
            return 'an Interface type';
163
        }
164
165 1
        if ($type instanceof UnionType) {
166 1
            return 'a Union type';
167
        }
168
169
        if ($type instanceof EnumType) {
170
            return 'an Enum type';
171
        }
172
173
        if ($type instanceof InputObjectType) {
174
            return 'an Input type';
175
        }
176
177
        throw new TypeError('unknown type ' . $type->name);
178
    }
179
180
    /**
181
     * @return string[][]
182
     */
183 2
    public static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes(
184
        Schema $oldSchema,
185
        Schema $newSchema
186
    ) {
187 2
        $oldTypeMap = $oldSchema->getTypeMap();
188 2
        $newTypeMap = $newSchema->getTypeMap();
189
190 2
        $breakingChanges = [];
191 2
        foreach ($oldTypeMap as $typeName => $oldType) {
192 2
            $newType = $newTypeMap[$typeName] ?? null;
193 2
            if (! ($oldType instanceof ObjectType || $oldType instanceof InterfaceType) ||
194 2
                ! ($newType instanceof ObjectType || $newType instanceof InterfaceType) ||
195 2
                ! ($newType instanceof $oldType)
196
            ) {
197 2
                continue;
198
            }
199
200 2
            $oldTypeFieldsDef = $oldType->getFields();
201 2
            $newTypeFieldsDef = $newType->getFields();
202 2
            foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) {
203
                // Check if the field is missing on the type in the new schema.
204 2
                if (! isset($newTypeFieldsDef[$fieldName])) {
205 2
                    $breakingChanges[] = [
206 2
                        'type'        => self::BREAKING_CHANGE_FIELD_REMOVED,
207 2
                        'description' => "${typeName}.${fieldName} was removed.",
208
                    ];
209
                } else {
210 2
                    $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
211 2
                    $newFieldType = $newTypeFieldsDef[$fieldName]->getType();
212 2
                    $isSafe       = self::isChangeSafeForObjectOrInterfaceField(
213 2
                        $oldFieldType,
214 2
                        $newFieldType
215
                    );
216 2
                    if (! $isSafe) {
217 2
                        $oldFieldTypeString = $oldFieldType instanceof NamedType && $oldFieldType instanceof Type
218 2
                            ? $oldFieldType->name
219 2
                            : $oldFieldType;
220 2
                        $newFieldTypeString = $newFieldType instanceof NamedType && $newFieldType instanceof Type
221 2
                            ? $newFieldType->name
222 2
                            : $newFieldType;
223 2
                        $breakingChanges[]  = [
224 2
                            'type'        => self::BREAKING_CHANGE_FIELD_CHANGED_KIND,
225 2
                            'description' => "${typeName}.${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}.",
226
                        ];
227
                    }
228
                }
229
            }
230
        }
231
232 2
        return $breakingChanges;
233
    }
234
235
    /**
236
     * @return bool
237
     */
238 2
    private static function isChangeSafeForObjectOrInterfaceField(
239
        Type $oldType,
240
        Type $newType
241
    ) {
242 2
        if ($oldType instanceof NamedType) {
243
            return // if they're both named types, see if their names are equivalent
244 2
                ($newType instanceof NamedType && $oldType->name === $newType->name) ||
245
                // moving from nullable to non-null of the same underlying type is safe
246 2
                ($newType instanceof NonNull &&
247 2
                    self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType())
248
                );
249
        }
250
251 2
        if ($oldType instanceof ListOfType) {
252
            return // if they're both lists, make sure the underlying types are compatible
253 2
                ($newType instanceof ListOfType &&
254 2
                    self::isChangeSafeForObjectOrInterfaceField(
255 2
                        $oldType->getWrappedType(),
256 2
                        $newType->getWrappedType()
257
                    )) ||
258
                // moving from nullable to non-null of the same underlying type is safe
259 1
                ($newType instanceof NonNull &&
260 2
                    self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType()));
261
        }
262
263 2
        if ($oldType instanceof NonNull) {
264
            // if they're both non-null, make sure the underlying types are compatible
265 2
            return $newType instanceof NonNull &&
266 2
                self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType());
267
        }
268
269
        return false;
270
    }
271
272
    /**
273
     * @return array<string, array<int, array<string, string>>>
274
     */
275 5
    public static function findFieldsThatChangedTypeOnInputObjectTypes(
276
        Schema $oldSchema,
277
        Schema $newSchema
278
    ) {
279 5
        $oldTypeMap = $oldSchema->getTypeMap();
280 5
        $newTypeMap = $newSchema->getTypeMap();
281
282 5
        $breakingChanges  = [];
283 5
        $dangerousChanges = [];
284 5
        foreach ($oldTypeMap as $typeName => $oldType) {
285 5
            $newType = $newTypeMap[$typeName] ?? null;
286 5
            if (! ($oldType instanceof InputObjectType) || ! ($newType instanceof InputObjectType)) {
287 5
                continue;
288
            }
289
290 3
            $oldTypeFieldsDef = $oldType->getFields();
291 3
            $newTypeFieldsDef = $newType->getFields();
292 3
            foreach (array_keys($oldTypeFieldsDef) as $fieldName) {
293 3
                if (! isset($newTypeFieldsDef[$fieldName])) {
294 1
                    $breakingChanges[] = [
295 1
                        'type'        => self::BREAKING_CHANGE_FIELD_REMOVED,
296 1
                        'description' => "${typeName}.${fieldName} was removed.",
297
                    ];
298
                } else {
299 3
                    $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
300 3
                    $newFieldType = $newTypeFieldsDef[$fieldName]->getType();
301
302 3
                    $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg(
303 3
                        $oldFieldType,
304 3
                        $newFieldType
305
                    );
306 3
                    if (! $isSafe) {
307 1
                        if ($oldFieldType instanceof NamedType) {
308 1
                            $oldFieldTypeString = $oldFieldType->name;
309
                        } else {
310 1
                            $oldFieldTypeString = $oldFieldType;
311
                        }
312 1
                        if ($newFieldType instanceof NamedType) {
313 1
                            $newFieldTypeString = $newFieldType->name;
314
                        } else {
315 1
                            $newFieldTypeString = $newFieldType;
316
                        }
317 1
                        $breakingChanges[] = [
318 1
                            'type'        => self::BREAKING_CHANGE_FIELD_CHANGED_KIND,
319 3
                            'description' => "${typeName}.${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}.",
320
                        ];
321
                    }
322
                }
323
            }
324
            // Check if a field was added to the input object type
325 3
            foreach ($newTypeFieldsDef as $fieldName => $fieldDef) {
326 3
                if (isset($oldTypeFieldsDef[$fieldName])) {
327 3
                    continue;
328
                }
329
330 2
                $newTypeName = $newType->name;
331 2
                if ($fieldDef->getType() instanceof NonNull) {
332 1
                    $breakingChanges[] = [
333 1
                        'type'        => self::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED,
334 1
                        'description' => "A non-null field ${fieldName} on input type ${newTypeName} was added.",
335
                    ];
336
                } else {
337 2
                    $dangerousChanges[] = [
338 2
                        'type'        => self::DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED,
339 3
                        'description' => "A nullable field ${fieldName} on input type ${newTypeName} was added.",
340
                    ];
341
                }
342
            }
343
        }
344
345
        return [
346 5
            'breakingChanges'  => $breakingChanges,
347 5
            'dangerousChanges' => $dangerousChanges,
348
        ];
349
    }
350
351
    /**
352
     * @return bool
353
     */
354 12
    private static function isChangeSafeForInputObjectFieldOrFieldArg(
355
        Type $oldType,
356
        Type $newType
357
    ) {
358 12
        if ($oldType instanceof NamedType) {
359 12
            if (! $newType instanceof NamedType) {
360 2
                return false;
361
            }
362
363
            // if they're both named types, see if their names are equivalent
364 12
            return $oldType->name === $newType->name;
365
        }
366
367 4
        if ($oldType instanceof ListOfType) {
368
            // if they're both lists, make sure the underlying types are compatible
369 2
            return $newType instanceof ListOfType &&
370 2
                self::isChangeSafeForInputObjectFieldOrFieldArg(
371 2
                    $oldType->getWrappedType(),
372 2
                    $newType->getWrappedType()
373
                );
374
        }
375
376 4
        if ($oldType instanceof NonNull) {
377
            return // if they're both non-null, make sure the underlying types are
378
                // compatible
379 4
                ($newType instanceof NonNull &&
380 3
                    self::isChangeSafeForInputObjectFieldOrFieldArg(
381 3
                        $oldType->getWrappedType(),
382 3
                        $newType->getWrappedType()
383
                    )) ||
384
                // moving from non-null to nullable of the same underlying type is safe
385 3
                ! ($newType instanceof NonNull) &&
386 4
                self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType);
387
        }
388
389
        return false;
390
    }
391
392
    /**
393
     * Given two schemas, returns an Array containing descriptions of any breaking
394
     * changes in the newSchema related to removing types from a union type.
395
     *
396
     * @return string[][]
397
     */
398 2
    public static function findTypesRemovedFromUnions(
399
        Schema $oldSchema,
400
        Schema $newSchema
401
    ) {
402 2
        $oldTypeMap = $oldSchema->getTypeMap();
403 2
        $newTypeMap = $newSchema->getTypeMap();
404
405 2
        $typesRemovedFromUnion = [];
406 2
        foreach ($oldTypeMap as $typeName => $oldType) {
407 2
            $newType = $newTypeMap[$typeName] ?? null;
408 2
            if (! ($oldType instanceof UnionType) || ! ($newType instanceof UnionType)) {
409 2
                continue;
410
            }
411 2
            $typeNamesInNewUnion = [];
412 2
            foreach ($newType->getTypes() as $type) {
413 2
                $typeNamesInNewUnion[$type->name] = true;
414
            }
415 2
            foreach ($oldType->getTypes() as $type) {
416 2
                if (isset($typeNamesInNewUnion[$type->name])) {
417 2
                    continue;
418
                }
419
420 2
                $typesRemovedFromUnion[] = [
421 2
                    'type'        => self::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION,
422 2
                    'description' => sprintf('%s was removed from union type %s.', $type->name, $typeName),
423
                ];
424
            }
425
        }
426
427 2
        return $typesRemovedFromUnion;
428
    }
429
430
    /**
431
     * Given two schemas, returns an Array containing descriptions of any breaking
432
     * changes in the newSchema related to removing values from an enum type.
433
     *
434
     * @return string[][]
435
     */
436 2
    public static function findValuesRemovedFromEnums(
437
        Schema $oldSchema,
438
        Schema $newSchema
439
    ) {
440 2
        $oldTypeMap = $oldSchema->getTypeMap();
441 2
        $newTypeMap = $newSchema->getTypeMap();
442
443 2
        $valuesRemovedFromEnums = [];
444 2
        foreach ($oldTypeMap as $typeName => $oldType) {
445 2
            $newType = $newTypeMap[$typeName] ?? null;
446 2
            if (! ($oldType instanceof EnumType) || ! ($newType instanceof EnumType)) {
447 2
                continue;
448
            }
449 2
            $valuesInNewEnum = [];
450 2
            foreach ($newType->getValues() as $value) {
451 2
                $valuesInNewEnum[$value->name] = true;
452
            }
453 2
            foreach ($oldType->getValues() as $value) {
454 2
                if (isset($valuesInNewEnum[$value->name])) {
455 2
                    continue;
456
                }
457
458 2
                $valuesRemovedFromEnums[] = [
459 2
                    'type'        => self::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM,
460 2
                    'description' => sprintf('%s was removed from enum type %s.', $value->name, $typeName),
461
                ];
462
            }
463
        }
464
465 2
        return $valuesRemovedFromEnums;
466
    }
467
468
    /**
469
     * Given two schemas, returns an Array containing descriptions of any
470
     * breaking or dangerous changes in the newSchema related to arguments
471
     * (such as removal or change of type of an argument, or a change in an
472
     * argument's default value).
473
     *
474
     * @return array<string, array<int,array<string, string>>>
475
     */
476 9
    public static function findArgChanges(
477
        Schema $oldSchema,
478
        Schema $newSchema
479
    ) {
480 9
        $oldTypeMap = $oldSchema->getTypeMap();
481 9
        $newTypeMap = $newSchema->getTypeMap();
482
483 9
        $breakingChanges  = [];
484 9
        $dangerousChanges = [];
485
486 9
        foreach ($oldTypeMap as $typeName => $oldType) {
487 9
            $newType = $newTypeMap[$typeName] ?? null;
488 9
            if (! ($oldType instanceof ObjectType || $oldType instanceof InterfaceType) ||
489 9
                ! ($newType instanceof ObjectType || $newType instanceof InterfaceType) ||
490 9
                ! ($newType instanceof $oldType)
491
            ) {
492 9
                continue;
493
            }
494
495 9
            $oldTypeFields = $oldType->getFields();
496 9
            $newTypeFields = $newType->getFields();
497
498 9
            foreach ($oldTypeFields as $fieldName => $oldField) {
499 9
                if (! isset($newTypeFields[$fieldName])) {
500 1
                    continue;
501
                }
502
503 9
                foreach ($oldField->args as $oldArgDef) {
504 9
                    $newArgs   = $newTypeFields[$fieldName]->args;
505 9
                    $newArgDef = Utils::find(
506 9
                        $newArgs,
507
                        static function ($arg) use ($oldArgDef) {
508 9
                            return $arg->name === $oldArgDef->name;
509 9
                        }
510
                    );
511 9
                    if ($newArgDef !== null) {
512 9
                        $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg(
513 9
                            $oldArgDef->getType(),
514 9
                            $newArgDef->getType()
515
                        );
516
                        /** @var ScalarType|EnumType|InputObjectType|ListOfType|NonNull $oldArgType */
517 9
                        $oldArgType = $oldArgDef->getType();
518 9
                        $oldArgName = $oldArgDef->name;
519 9
                        if (! $isSafe) {
520 2
                            $newArgType        = $newArgDef->getType();
521 2
                            $breakingChanges[] = [
522 2
                                'type'        => self::BREAKING_CHANGE_ARG_CHANGED_KIND,
523 2
                                'description' => "${typeName}.${fieldName} arg ${oldArgName} has changed type from ${oldArgType} to ${newArgType}",
524
                            ];
525 9
                        } elseif ($oldArgDef->defaultValueExists() && $oldArgDef->defaultValue !== $newArgDef->defaultValue) {
526 2
                            $dangerousChanges[] = [
527 2
                                'type'        => self::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED,
528 9
                                'description' => "${typeName}.${fieldName} arg ${oldArgName} has changed defaultValue",
529
                            ];
530
                        }
531
                    } else {
532 1
                        $breakingChanges[] = [
533 1
                            'type'        => self::BREAKING_CHANGE_ARG_REMOVED,
534 1
                            'description' => sprintf(
535 1
                                '%s.%s arg %s was removed',
536 1
                                $typeName,
537 1
                                $fieldName,
538 1
                                $oldArgDef->name
539
                            ),
540
                        ];
541
                    }
542
                    // Check if a non-null arg was added to the field
543 9
                    foreach ($newTypeFields[$fieldName]->args as $newTypeFieldArgDef) {
544 9
                        $oldArgs   = $oldTypeFields[$fieldName]->args;
545 9
                        $oldArgDef = Utils::find(
546 9
                            $oldArgs,
547
                            static function ($arg) use ($newTypeFieldArgDef) {
548 9
                                return $arg->name === $newTypeFieldArgDef->name;
549 9
                            }
550
                        );
551
552 9
                        if ($oldArgDef !== null) {
553 9
                            continue;
554
                        }
555
556 2
                        $newTypeName = $newType->name;
557 2
                        $newArgName  = $newTypeFieldArgDef->name;
558 2
                        if ($newTypeFieldArgDef->getType() instanceof NonNull) {
559 1
                            $breakingChanges[] = [
560 1
                                'type'        => self::BREAKING_CHANGE_NON_NULL_ARG_ADDED,
561 1
                                'description' => "A non-null arg ${newArgName} on ${newTypeName}.${fieldName} was added",
562
                            ];
563
                        } else {
564 2
                            $dangerousChanges[] = [
565 2
                                'type'        => self::DANGEROUS_CHANGE_NULLABLE_ARG_ADDED,
566 9
                                'description' => "A nullable arg ${newArgName} on ${newTypeName}.${fieldName} was added",
567
                            ];
568
                        }
569
                    }
570
                }
571
            }
572
        }
573
574
        return [
575 9
            'breakingChanges'  => $breakingChanges,
576 9
            'dangerousChanges' => $dangerousChanges,
577
        ];
578
    }
579
580
    /**
581
     * @return string[][]
582
     */
583 2
    public static function findInterfacesRemovedFromObjectTypes(
584
        Schema $oldSchema,
585
        Schema $newSchema
586
    ) {
587 2
        $oldTypeMap      = $oldSchema->getTypeMap();
588 2
        $newTypeMap      = $newSchema->getTypeMap();
589 2
        $breakingChanges = [];
590
591 2
        foreach ($oldTypeMap as $typeName => $oldType) {
592 2
            $newType = $newTypeMap[$typeName] ?? null;
593 2
            if (! ($oldType instanceof ObjectType) || ! ($newType instanceof ObjectType)) {
594 2
                continue;
595
            }
596
597 2
            $oldInterfaces = $oldType->getInterfaces();
598 2
            $newInterfaces = $newType->getInterfaces();
599 2
            foreach ($oldInterfaces as $oldInterface) {
600 2
                $interface = Utils::find(
601 2
                    $newInterfaces,
602
                    static function (InterfaceType $interface) use ($oldInterface) : bool {
603
                        return $interface->name === $oldInterface->name;
604 2
                    }
605
                );
606 2
                if ($interface !== null) {
607
                    continue;
608
                }
609
610 2
                $breakingChanges[] = [
611 2
                    'type'        => self::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT,
612 2
                    'description' => sprintf('%s no longer implements interface %s.', $typeName, $oldInterface->name),
613
                ];
614
            }
615
        }
616
617 2
        return $breakingChanges;
618
    }
619
620
    /**
621
     * @return string[][]
622
     */
623 3
    public static function findRemovedDirectives(Schema $oldSchema, Schema $newSchema)
624
    {
625 3
        $removedDirectives = [];
626
627 3
        $newSchemaDirectiveMap = self::getDirectiveMapForSchema($newSchema);
628 3
        foreach ($oldSchema->getDirectives() as $directive) {
629 3
            if (isset($newSchemaDirectiveMap[$directive->name])) {
630 3
                continue;
631
            }
632
633 3
            $removedDirectives[] = [
634 3
                'type'        => self::BREAKING_CHANGE_DIRECTIVE_REMOVED,
635 3
                'description' => sprintf('%s was removed', $directive->name),
636
            ];
637
        }
638
639 3
        return $removedDirectives;
640
    }
641
642 6
    private static function getDirectiveMapForSchema(Schema $schema)
643
    {
644 6
        return Utils::keyMap(
645 6
            $schema->getDirectives(),
646
            static function ($dir) {
647 6
                return $dir->name;
648 6
            }
649
        );
650
    }
651
652 2
    public static function findRemovedDirectiveArgs(Schema $oldSchema, Schema $newSchema)
653
    {
654 2
        $removedDirectiveArgs  = [];
655 2
        $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
656
657 2
        foreach ($newSchema->getDirectives() as $newDirective) {
658 2
            if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
659
                continue;
660
            }
661
662 2
            foreach (self::findRemovedArgsForDirectives(
663 2
                $oldSchemaDirectiveMap[$newDirective->name],
664 2
                $newDirective
665
            ) as $arg) {
666 2
                $removedDirectiveArgs[] = [
667 2
                    'type'        => self::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED,
668 2
                    'description' => sprintf('%s was removed from %s', $arg->name, $newDirective->name),
669
                ];
670
            }
671
        }
672
673 2
        return $removedDirectiveArgs;
674
    }
675
676 2
    public static function findRemovedArgsForDirectives(Directive $oldDirective, Directive $newDirective)
677
    {
678 2
        $removedArgs = [];
679 2
        $newArgMap   = self::getArgumentMapForDirective($newDirective);
680 2
        foreach ($oldDirective->args as $arg) {
681 2
            if (isset($newArgMap[$arg->name])) {
682
                continue;
683
            }
684
685 2
            $removedArgs[] = $arg;
686
        }
687
688 2
        return $removedArgs;
689
    }
690
691 3
    private static function getArgumentMapForDirective(Directive $directive)
692
    {
693 3
        return Utils::keyMap(
694 3
            $directive->args ?: [],
695
            static function ($arg) {
696 1
                return $arg->name;
697 3
            }
698
        );
699
    }
700
701 2
    public static function findAddedNonNullDirectiveArgs(Schema $oldSchema, Schema $newSchema)
702
    {
703 2
        $addedNonNullableArgs  = [];
704 2
        $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
705
706 2
        foreach ($newSchema->getDirectives() as $newDirective) {
707 2
            if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
708
                continue;
709
            }
710
711 2
            foreach (self::findAddedArgsForDirective(
712 2
                $oldSchemaDirectiveMap[$newDirective->name],
713 2
                $newDirective
714
            ) as $arg) {
715 2
                if (! $arg->getType() instanceof NonNull) {
716
                    continue;
717
                }
718 2
                $addedNonNullableArgs[] = [
719 2
                    'type'        => self::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED,
720 2
                    'description' => sprintf(
721 2
                        'A non-null arg %s on directive %s was added',
722 2
                        $arg->name,
723 2
                        $newDirective->name
724
                    ),
725
                ];
726
            }
727
        }
728
729 2
        return $addedNonNullableArgs;
730
    }
731
732
    /**
733
     * @return FieldArgument[]
734
     */
735 2
    public static function findAddedArgsForDirective(Directive $oldDirective, Directive $newDirective)
736
    {
737 2
        $addedArgs = [];
738 2
        $oldArgMap = self::getArgumentMapForDirective($oldDirective);
739 2
        foreach ($newDirective->args as $arg) {
740 2
            if (isset($oldArgMap[$arg->name])) {
741
                continue;
742
            }
743
744 2
            $addedArgs[] = $arg;
745
        }
746
747 2
        return $addedArgs;
748
    }
749
750
    /**
751
     * @return string[][]
752
     */
753 2
    public static function findRemovedDirectiveLocations(Schema $oldSchema, Schema $newSchema)
754
    {
755 2
        $removedLocations      = [];
756 2
        $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
757
758 2
        foreach ($newSchema->getDirectives() as $newDirective) {
759 2
            if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
760
                continue;
761
            }
762
763 2
            foreach (self::findRemovedLocationsForDirective(
764 2
                $oldSchemaDirectiveMap[$newDirective->name],
765 2
                $newDirective
766
            ) as $location) {
767 2
                $removedLocations[] = [
768 2
                    'type'        => self::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED,
769 2
                    'description' => sprintf('%s was removed from %s', $location, $newDirective->name),
770
                ];
771
            }
772
        }
773
774 2
        return $removedLocations;
775
    }
776
777 3
    public static function findRemovedLocationsForDirective(Directive $oldDirective, Directive $newDirective)
778
    {
779 3
        $removedLocations = [];
780 3
        $newLocationSet   = array_flip($newDirective->locations);
781 3
        foreach ($oldDirective->locations as $oldLocation) {
782 3
            if (array_key_exists($oldLocation, $newLocationSet)) {
783 3
                continue;
784
            }
785
786 3
            $removedLocations[] = $oldLocation;
787
        }
788
789 3
        return $removedLocations;
790
    }
791
792
    /**
793
     * Given two schemas, returns an Array containing descriptions of all the types
794
     * of potentially dangerous changes covered by the other functions down below.
795
     *
796
     * @return string[][]
797
     */
798 1
    public static function findDangerousChanges(Schema $oldSchema, Schema $newSchema)
799
    {
800 1
        return array_merge(
801 1
            self::findArgChanges($oldSchema, $newSchema)['dangerousChanges'],
802 1
            self::findValuesAddedToEnums($oldSchema, $newSchema),
803 1
            self::findInterfacesAddedToObjectTypes($oldSchema, $newSchema),
804 1
            self::findTypesAddedToUnions($oldSchema, $newSchema),
805 1
            self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['dangerousChanges']
806
        );
807
    }
808
809
    /**
810
     * Given two schemas, returns an Array containing descriptions of any dangerous
811
     * changes in the newSchema related to adding values to an enum type.
812
     *
813
     * @return string[][]
814
     */
815 2
    public static function findValuesAddedToEnums(
816
        Schema $oldSchema,
817
        Schema $newSchema
818
    ) {
819 2
        $oldTypeMap = $oldSchema->getTypeMap();
820 2
        $newTypeMap = $newSchema->getTypeMap();
821
822 2
        $valuesAddedToEnums = [];
823 2
        foreach ($oldTypeMap as $typeName => $oldType) {
824 2
            $newType = $newTypeMap[$typeName] ?? null;
825 2
            if (! ($oldType instanceof EnumType) || ! ($newType instanceof EnumType)) {
826 2
                continue;
827
            }
828 2
            $valuesInOldEnum = [];
829 2
            foreach ($oldType->getValues() as $value) {
830 2
                $valuesInOldEnum[$value->name] = true;
831
            }
832 2
            foreach ($newType->getValues() as $value) {
833 2
                if (isset($valuesInOldEnum[$value->name])) {
834 2
                    continue;
835
                }
836
837 2
                $valuesAddedToEnums[] = [
838 2
                    'type'        => self::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM,
839 2
                    'description' => sprintf('%s was added to enum type %s.', $value->name, $typeName),
840
                ];
841
            }
842
        }
843
844 2
        return $valuesAddedToEnums;
845
    }
846
847
    /**
848
     * @return string[][]
849
     */
850 2
    public static function findInterfacesAddedToObjectTypes(
851
        Schema $oldSchema,
852
        Schema $newSchema
853
    ) {
854 2
        $oldTypeMap                   = $oldSchema->getTypeMap();
855 2
        $newTypeMap                   = $newSchema->getTypeMap();
856 2
        $interfacesAddedToObjectTypes = [];
857
858 2
        foreach ($newTypeMap as $typeName => $newType) {
859 2
            $oldType = $oldTypeMap[$typeName] ?? null;
860 2
            if (! ($oldType instanceof ObjectType) || ! ($newType instanceof ObjectType)) {
861 2
                continue;
862
            }
863
864 2
            $oldInterfaces = $oldType->getInterfaces();
865 2
            $newInterfaces = $newType->getInterfaces();
866 2
            foreach ($newInterfaces as $newInterface) {
867 2
                $interface = Utils::find(
868 2
                    $oldInterfaces,
869
                    static function (InterfaceType $interface) use ($newInterface) : bool {
870
                        return $interface->name === $newInterface->name;
871 2
                    }
872
                );
873
874 2
                if ($interface !== null) {
875
                    continue;
876
                }
877
878 2
                $interfacesAddedToObjectTypes[] = [
879 2
                    'type'        => self::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT,
880 2
                    'description' => sprintf(
881 2
                        '%s added to interfaces implemented by %s.',
882 2
                        $newInterface->name,
883 2
                        $typeName
884
                    ),
885
                ];
886
            }
887
        }
888
889 2
        return $interfacesAddedToObjectTypes;
890
    }
891
892
    /**
893
     * Given two schemas, returns an Array containing descriptions of any dangerous
894
     * changes in the newSchema related to adding types to a union type.
895
     *
896
     * @return string[][]
897
     */
898 2
    public static function findTypesAddedToUnions(
899
        Schema $oldSchema,
900
        Schema $newSchema
901
    ) {
902 2
        $oldTypeMap = $oldSchema->getTypeMap();
903 2
        $newTypeMap = $newSchema->getTypeMap();
904
905 2
        $typesAddedToUnion = [];
906 2
        foreach ($newTypeMap as $typeName => $newType) {
907 2
            $oldType = $oldTypeMap[$typeName] ?? null;
908 2
            if (! ($oldType instanceof UnionType) || ! ($newType instanceof UnionType)) {
909 2
                continue;
910
            }
911
912 2
            $typeNamesInOldUnion = [];
913 2
            foreach ($oldType->getTypes() as $type) {
914 2
                $typeNamesInOldUnion[$type->name] = true;
915
            }
916 2
            foreach ($newType->getTypes() as $type) {
917 2
                if (isset($typeNamesInOldUnion[$type->name])) {
918 2
                    continue;
919
                }
920
921 2
                $typesAddedToUnion[] = [
922 2
                    'type'        => self::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION,
923 2
                    'description' => sprintf('%s was added to union type %s.', $type->name, $typeName),
924
                ];
925
            }
926
        }
927
928 2
        return $typesAddedToUnion;
929
    }
930
}
931
932
class_alias(BreakingChangesFinder::class, 'GraphQL\Utils\FindBreakingChanges');
933