Completed
Push — master ( 974258...005b1a )
by Vladimir
19:57 queued 16:19
created

BreakingChangesFinder   F

Complexity

Total Complexity 146

Size/Duplication

Total Lines 888
Duplicated Lines 0 %

Test Coverage

Coverage 89.57%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 146
eloc 452
dl 0
loc 888
ccs 395
cts 441
cp 0.8957
rs 2
c 1
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A findRemovedLocationsForDirective() 0 13 3
B findInterfacesAddedToObjectTypes() 0 40 6
A findRemovedDirectiveLocations() 0 22 4
A findRemovedArgsForDirectives() 0 13 3
A findAddedNonNullDirectiveArgs() 0 29 5
B findValuesAddedToEnums() 0 30 7
A findDangerousChanges() 0 8 1
B findTypesAddedToUnions() 0 31 7
A findRemovedDirectiveArgs() 0 22 4
A findAddedArgsForDirective() 0 13 3
B findInterfacesRemovedFromObjectTypes() 0 35 6
A findRemovedTypes() 0 20 3
A getDirectiveMapForSchema() 0 6 1
C findFieldsThatChangedTypeOnObjectOrInterfaceTypes() 0 50 12
B findValuesRemovedFromEnums() 0 30 7
B findTypesRemovedFromUnions() 0 30 7
B isChangeSafeForObjectOrInterfaceField() 0 32 11
C findFieldsThatChangedTypeOnInputObjectTypes() 0 69 12
B isChangeSafeForInputObjectFieldOrFieldArg() 0 32 9
A getArgumentMapForDirective() 0 6 2
A findBreakingChanges() 0 15 1
A findTypesThatChangedKind() 0 30 5
C findArgChanges() 0 100 17
A findRemovedDirectives() 0 17 3
B typeKindName() 0 27 7

How to fix   Complexity   

Complex Class

Complex classes like BreakingChangesFinder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BreakingChangesFinder, and based on these observations, apply Extract Interface, too.

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