Passed
Pull Request — master (#384)
by
unknown
04:58
created

findRemovedLocationsForDirective()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

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