Completed
Push — master ( a01b08...b72ba3 )
by Vladimir
16s queued 14s
created

BreakingChangesFinder   F

Complexity

Total Complexity 146

Size/Duplication

Total Lines 888
Duplicated Lines 0 %

Test Coverage

Coverage 95.92%

Importance

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

25 Methods

Rating   Name   Duplication   Size   Complexity  
A findRemovedLocationsForDirective() 0 13 3
B findInterfacesAddedToObjectTypes() 0 40 6
A findRemovedDirectiveLocations() 0 22 4
A getArgumentMapForDirective() 0 6 2
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
A findBreakingChanges() 0 15 1
C findFieldsThatChangedTypeOnObjectOrInterfaceTypes() 0 50 12
A findTypesThatChangedKind() 0 30 5
B findValuesRemovedFromEnums() 0 30 7
C findArgChanges() 0 100 17
A findRemovedDirectives() 0 17 3
B typeKindName() 0 27 7
B findTypesRemovedFromUnions() 0 30 7
B isChangeSafeForObjectOrInterfaceField() 0 32 11
C findFieldsThatChangedTypeOnInputObjectTypes() 0 69 12
B isChangeSafeForInputObjectFieldOrFieldArg() 0 32 9

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 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
218 2
                            ? $oldFieldType->name
219 2
                            : $oldFieldType;
220 2
                        $newFieldTypeString = $newFieldType instanceof NamedType
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 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
                        $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 5
            'breakingChanges'  => $breakingChanges,
343 5
            'dangerousChanges' => $dangerousChanges,
344
        ];
345
    }
346
347
    /**
348
     * @return bool
349
     */
350 12
    private static function isChangeSafeForInputObjectFieldOrFieldArg(
351
        Type $oldType,
352
        Type $newType
353
    ) {
354 12
        if ($oldType instanceof NamedType) {
355
            // if they're both named types, see if their names are equivalent
356 12
            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 2
    public static function findTypesRemovedFromUnions(
391
        Schema $oldSchema,
392
        Schema $newSchema
393
    ) {
394 2
        $oldTypeMap = $oldSchema->getTypeMap();
395 2
        $newTypeMap = $newSchema->getTypeMap();
396
397 2
        $typesRemovedFromUnion = [];
398 2
        foreach ($oldTypeMap as $typeName => $oldType) {
399 2
            $newType = $newTypeMap[$typeName] ?? null;
400 2
            if (! ($oldType instanceof UnionType) || ! ($newType instanceof UnionType)) {
401 2
                continue;
402
            }
403 2
            $typeNamesInNewUnion = [];
404 2
            foreach ($newType->getTypes() as $type) {
405 2
                $typeNamesInNewUnion[$type->name] = true;
406
            }
407 2
            foreach ($oldType->getTypes() as $type) {
408 2
                if (isset($typeNamesInNewUnion[$type->name])) {
409 2
                    continue;
410
                }
411
412 2
                $typesRemovedFromUnion[] = [
413 2
                    'type'        => self::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION,
414 2
                    'description' => sprintf('%s was removed from union type %s.', $type->name, $typeName),
415
                ];
416
            }
417
        }
418
419 2
        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 2
    public static function findValuesRemovedFromEnums(
429
        Schema $oldSchema,
430
        Schema $newSchema
431
    ) {
432 2
        $oldTypeMap = $oldSchema->getTypeMap();
433 2
        $newTypeMap = $newSchema->getTypeMap();
434
435 2
        $valuesRemovedFromEnums = [];
436 2
        foreach ($oldTypeMap as $typeName => $oldType) {
437 2
            $newType = $newTypeMap[$typeName] ?? null;
438 2
            if (! ($oldType instanceof EnumType) || ! ($newType instanceof EnumType)) {
439 2
                continue;
440
            }
441 2
            $valuesInNewEnum = [];
442 2
            foreach ($newType->getValues() as $value) {
443 2
                $valuesInNewEnum[$value->name] = true;
444
            }
445 2
            foreach ($oldType->getValues() as $value) {
446 2
                if (isset($valuesInNewEnum[$value->name])) {
447 2
                    continue;
448
                }
449
450 2
                $valuesRemovedFromEnums[] = [
451 2
                    'type'        => self::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM,
452 2
                    'description' => sprintf('%s was removed from enum type %s.', $value->name, $typeName),
453
                ];
454
            }
455
        }
456
457 2
        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 9
    public static function findArgChanges(
469
        Schema $oldSchema,
470
        Schema $newSchema
471
    ) {
472 9
        $oldTypeMap = $oldSchema->getTypeMap();
473 9
        $newTypeMap = $newSchema->getTypeMap();
474
475 9
        $breakingChanges  = [];
476 9
        $dangerousChanges = [];
477
478 9
        foreach ($oldTypeMap as $typeName => $oldType) {
479 9
            $newType = $newTypeMap[$typeName] ?? null;
480 9
            if (! ($oldType instanceof ObjectType || $oldType instanceof InterfaceType) ||
481 9
                ! ($newType instanceof ObjectType || $newType instanceof InterfaceType) ||
482 9
                ! ($newType instanceof $oldType)
483
            ) {
484 9
                continue;
485
            }
486
487 9
            $oldTypeFields = $oldType->getFields();
488 9
            $newTypeFields = $newType->getFields();
489
490 9
            foreach ($oldTypeFields as $fieldName => $oldField) {
491 9
                if (! isset($newTypeFields[$fieldName])) {
492 1
                    continue;
493
                }
494
495 9
                foreach ($oldField->args as $oldArgDef) {
496 9
                    $newArgs   = $newTypeFields[$fieldName]->args;
497 9
                    $newArgDef = Utils::find(
498 9
                        $newArgs,
499
                        static function ($arg) use ($oldArgDef) {
500 9
                            return $arg->name === $oldArgDef->name;
501 9
                        }
502
                    );
503 9
                    if ($newArgDef !== null) {
504 9
                        $isSafe     = self::isChangeSafeForInputObjectFieldOrFieldArg(
505 9
                            $oldArgDef->getType(),
506 9
                            $newArgDef->getType()
507
                        );
508 9
                        $oldArgType = $oldArgDef->getType();
509 9
                        $oldArgName = $oldArgDef->name;
510 9
                        if (! $isSafe) {
511 2
                            $newArgType        = $newArgDef->getType();
512 2
                            $breakingChanges[] = [
513 2
                                'type'        => self::BREAKING_CHANGE_ARG_CHANGED_KIND,
514 2
                                'description' => "${typeName}.${fieldName} arg ${oldArgName} has changed type from ${oldArgType} to ${newArgType}",
515
                            ];
516 9
                        } elseif ($oldArgDef->defaultValueExists() && $oldArgDef->defaultValue !== $newArgDef->defaultValue) {
517 2
                            $dangerousChanges[] = [
518 2
                                'type'        => self::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED,
519 9
                                '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 9
                    foreach ($newTypeFields[$fieldName]->args as $newTypeFieldArgDef) {
535 9
                        $oldArgs   = $oldTypeFields[$fieldName]->args;
536 9
                        $oldArgDef = Utils::find(
537 9
                            $oldArgs,
538
                            static function ($arg) use ($newTypeFieldArgDef) {
539 9
                                return $arg->name === $newTypeFieldArgDef->name;
540 9
                            }
541
                        );
542
543 9
                        if ($oldArgDef !== null) {
544 9
                            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 9
                                'description' => "A nullable arg ${newArgName} on ${newTypeName}.${fieldName} was added",
558
                            ];
559
                        }
560
                    }
561
                }
562
            }
563
        }
564
565
        return [
566 9
            'breakingChanges'  => $breakingChanges,
567 9
            'dangerousChanges' => $dangerousChanges,
568
        ];
569
    }
570
571
    /**
572
     * @return string[][]
573
     */
574 2
    public static function findInterfacesRemovedFromObjectTypes(
575
        Schema $oldSchema,
576
        Schema $newSchema
577
    ) {
578 2
        $oldTypeMap      = $oldSchema->getTypeMap();
579 2
        $newTypeMap      = $newSchema->getTypeMap();
580 2
        $breakingChanges = [];
581
582 2
        foreach ($oldTypeMap as $typeName => $oldType) {
583 2
            $newType = $newTypeMap[$typeName] ?? null;
584 2
            if (! ($oldType instanceof ObjectType) || ! ($newType instanceof ObjectType)) {
585 2
                continue;
586
            }
587
588 2
            $oldInterfaces = $oldType->getInterfaces();
589 2
            $newInterfaces = $newType->getInterfaces();
590 2
            foreach ($oldInterfaces as $oldInterface) {
591 2
                $interface = Utils::find(
592 2
                    $newInterfaces,
593
                    static function (InterfaceType $interface) use ($oldInterface) : bool {
594
                        return $interface->name === $oldInterface->name;
595 2
                    }
596
                );
597 2
                if ($interface !== null) {
598
                    continue;
599
                }
600
601 2
                $breakingChanges[] = [
602 2
                    'type'        => self::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT,
603 2
                    'description' => sprintf('%s no longer implements interface %s.', $typeName, $oldInterface->name),
604
                ];
605
            }
606
        }
607
608 2
        return $breakingChanges;
609
    }
610
611
    /**
612
     * @return string[][]
613
     */
614 3
    public static function findRemovedDirectives(Schema $oldSchema, Schema $newSchema)
615
    {
616 3
        $removedDirectives = [];
617
618 3
        $newSchemaDirectiveMap = self::getDirectiveMapForSchema($newSchema);
619 3
        foreach ($oldSchema->getDirectives() as $directive) {
620 3
            if (isset($newSchemaDirectiveMap[$directive->name])) {
621 3
                continue;
622
            }
623
624 3
            $removedDirectives[] = [
625 3
                'type'        => self::BREAKING_CHANGE_DIRECTIVE_REMOVED,
626 3
                'description' => sprintf('%s was removed', $directive->name),
627
            ];
628
        }
629
630 3
        return $removedDirectives;
631
    }
632
633 6
    private static function getDirectiveMapForSchema(Schema $schema)
634
    {
635 6
        return Utils::keyMap(
636 6
            $schema->getDirectives(),
637
            static function ($dir) {
638 6
                return $dir->name;
639 6
            }
640
        );
641
    }
642
643 2
    public static function findRemovedDirectiveArgs(Schema $oldSchema, Schema $newSchema)
644
    {
645 2
        $removedDirectiveArgs  = [];
646 2
        $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
647
648 2
        foreach ($newSchema->getDirectives() as $newDirective) {
649 2
            if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
650
                continue;
651
            }
652
653 2
            foreach (self::findRemovedArgsForDirectives(
654 2
                $oldSchemaDirectiveMap[$newDirective->name],
655 2
                $newDirective
656
            ) as $arg) {
657 2
                $removedDirectiveArgs[] = [
658 2
                    'type'        => self::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED,
659 2
                    'description' => sprintf('%s was removed from %s', $arg->name, $newDirective->name),
660
                ];
661
            }
662
        }
663
664 2
        return $removedDirectiveArgs;
665
    }
666
667 2
    public static function findRemovedArgsForDirectives(Directive $oldDirective, Directive $newDirective)
668
    {
669 2
        $removedArgs = [];
670 2
        $newArgMap   = self::getArgumentMapForDirective($newDirective);
671 2
        foreach ($oldDirective->args as $arg) {
672 2
            if (isset($newArgMap[$arg->name])) {
673
                continue;
674
            }
675
676 2
            $removedArgs[] = $arg;
677
        }
678
679 2
        return $removedArgs;
680
    }
681
682 3
    private static function getArgumentMapForDirective(Directive $directive)
683
    {
684 3
        return Utils::keyMap(
685 3
            $directive->args ?: [],
686
            static function ($arg) {
687 1
                return $arg->name;
688 3
            }
689
        );
690
    }
691
692 2
    public static function findAddedNonNullDirectiveArgs(Schema $oldSchema, Schema $newSchema)
693
    {
694 2
        $addedNonNullableArgs  = [];
695 2
        $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
696
697 2
        foreach ($newSchema->getDirectives() as $newDirective) {
698 2
            if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
699
                continue;
700
            }
701
702 2
            foreach (self::findAddedArgsForDirective(
703 2
                $oldSchemaDirectiveMap[$newDirective->name],
704 2
                $newDirective
705
            ) as $arg) {
706 2
                if (! $arg->getType() instanceof NonNull) {
707
                    continue;
708
                }
709 2
                $addedNonNullableArgs[] = [
710 2
                    'type'        => self::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED,
711 2
                    'description' => sprintf(
712 2
                        'A non-null arg %s on directive %s was added',
713 2
                        $arg->name,
714 2
                        $newDirective->name
715
                    ),
716
                ];
717
            }
718
        }
719
720 2
        return $addedNonNullableArgs;
721
    }
722
723
    /**
724
     * @return FieldArgument[]
725
     */
726 2
    public static function findAddedArgsForDirective(Directive $oldDirective, Directive $newDirective)
727
    {
728 2
        $addedArgs = [];
729 2
        $oldArgMap = self::getArgumentMapForDirective($oldDirective);
730 2
        foreach ($newDirective->args as $arg) {
731 2
            if (isset($oldArgMap[$arg->name])) {
732
                continue;
733
            }
734
735 2
            $addedArgs[] = $arg;
736
        }
737
738 2
        return $addedArgs;
739
    }
740
741
    /**
742
     * @return string[][]
743
     */
744 2
    public static function findRemovedDirectiveLocations(Schema $oldSchema, Schema $newSchema)
745
    {
746 2
        $removedLocations      = [];
747 2
        $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
748
749 2
        foreach ($newSchema->getDirectives() as $newDirective) {
750 2
            if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
751
                continue;
752
            }
753
754 2
            foreach (self::findRemovedLocationsForDirective(
755 2
                $oldSchemaDirectiveMap[$newDirective->name],
756 2
                $newDirective
757
            ) as $location) {
758 2
                $removedLocations[] = [
759 2
                    'type'        => self::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED,
760 2
                    'description' => sprintf('%s was removed from %s', $location, $newDirective->name),
761
                ];
762
            }
763
        }
764
765 2
        return $removedLocations;
766
    }
767
768 3
    public static function findRemovedLocationsForDirective(Directive $oldDirective, Directive $newDirective)
769
    {
770 3
        $removedLocations = [];
771 3
        $newLocationSet   = array_flip($newDirective->locations);
772 3
        foreach ($oldDirective->locations as $oldLocation) {
773 3
            if (array_key_exists($oldLocation, $newLocationSet)) {
774 3
                continue;
775
            }
776
777 3
            $removedLocations[] = $oldLocation;
778
        }
779
780 3
        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