Failed Conditions
Pull Request — master (#328)
by Šimon
04:02
created

BreakingChangesFinder::findDangerousChanges()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 8
ccs 6
cts 6
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
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 function array_flip;
25
use function array_key_exists;
26
use function array_keys;
27
use function array_merge;
28
use function class_alias;
29
use function sprintf;
30
31
class BreakingChangesFinder
32
{
33
    public const BREAKING_CHANGE_FIELD_CHANGED_KIND            = 'FIELD_CHANGED_KIND';
34
    public const BREAKING_CHANGE_FIELD_REMOVED                 = 'FIELD_REMOVED';
35
    public const BREAKING_CHANGE_TYPE_CHANGED_KIND             = 'TYPE_CHANGED_KIND';
36
    public const BREAKING_CHANGE_TYPE_REMOVED                  = 'TYPE_REMOVED';
37
    public const BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION       = 'TYPE_REMOVED_FROM_UNION';
38
    public const BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM       = 'VALUE_REMOVED_FROM_ENUM';
39
    public const BREAKING_CHANGE_ARG_REMOVED                   = 'ARG_REMOVED';
40
    public const BREAKING_CHANGE_ARG_CHANGED_KIND              = 'ARG_CHANGED_KIND';
41
    public const BREAKING_CHANGE_NON_NULL_ARG_ADDED            = 'NON_NULL_ARG_ADDED';
42
    public const BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED    = 'NON_NULL_INPUT_FIELD_ADDED';
43
    public const BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT = 'INTERFACE_REMOVED_FROM_OBJECT';
44
    public const BREAKING_CHANGE_DIRECTIVE_REMOVED             = 'DIRECTIVE_REMOVED';
45
    public const BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED         = 'DIRECTIVE_ARG_REMOVED';
46
    public const BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED    = 'DIRECTIVE_LOCATION_REMOVED';
47
    public const BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED  = 'NON_NULL_DIRECTIVE_ARG_ADDED';
48
    public const DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED    = 'ARG_DEFAULT_VALUE_CHANGE';
49
    public const DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM          = 'VALUE_ADDED_TO_ENUM';
50
    public const DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT    = 'INTERFACE_ADDED_TO_OBJECT';
51
    public const DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION          = 'TYPE_ADDED_TO_UNION';
52
    public const DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED   = 'NULLABLE_INPUT_FIELD_ADDED';
53
    public const DANGEROUS_CHANGE_NULLABLE_ARG_ADDED           = 'NULLABLE_ARG_ADDED';
54
55
    /**
56
     * Given two schemas, returns an Array containing descriptions of all the types
57
     * of breaking changes covered by the other functions down below.
58
     *
59
     * @return string[][]
60
     */
61 1
    public static function findBreakingChanges(Schema $oldSchema, Schema $newSchema)
62
    {
63 1
        return array_merge(
64 1
            self::findRemovedTypes($oldSchema, $newSchema),
65 1
            self::findTypesThatChangedKind($oldSchema, $newSchema),
66 1
            self::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema),
67 1
            self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['breakingChanges'],
68 1
            self::findTypesRemovedFromUnions($oldSchema, $newSchema),
69 1
            self::findValuesRemovedFromEnums($oldSchema, $newSchema),
70 1
            self::findArgChanges($oldSchema, $newSchema)['breakingChanges'],
71 1
            self::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema),
72 1
            self::findRemovedDirectives($oldSchema, $newSchema),
73 1
            self::findRemovedDirectiveArgs($oldSchema, $newSchema),
74 1
            self::findAddedNonNullDirectiveArgs($oldSchema, $newSchema),
75 1
            self::findRemovedDirectiveLocations($oldSchema, $newSchema)
76
        );
77
    }
78
79
    /**
80
     * Given two schemas, returns an Array containing descriptions of any breaking
81
     * changes in the newSchema related to removing an entire type.
82
     *
83
     * @return string[][]
84
     */
85 2
    public static function findRemovedTypes(
86
        Schema $oldSchema,
87
        Schema $newSchema
88
    ) {
89 2
        $oldTypeMap = $oldSchema->getTypeMap();
90 2
        $newTypeMap = $newSchema->getTypeMap();
91
92 2
        $breakingChanges = [];
93 2
        foreach (array_keys($oldTypeMap) as $typeName) {
94 2
            if (isset($newTypeMap[$typeName])) {
95 2
                continue;
96
            }
97
98 2
            $breakingChanges[] = [
99 2
                'type'        => self::BREAKING_CHANGE_TYPE_REMOVED,
100 2
                'description' => "${typeName} was removed.",
101
            ];
102
        }
103
104
        return $breakingChanges;
105
    }
106
107
    /**
108
     * Given two schemas, returns an Array containing descriptions of any breaking
109
     * changes in the newSchema related to changing the type of a type.
110
     *
111
     * @return string[][]
112
     */
113
    public static function findTypesThatChangedKind(
114
        Schema $oldSchema,
115
        Schema $newSchema
116
    ) {
117 2
        $oldTypeMap = $oldSchema->getTypeMap();
118 2
        $newTypeMap = $newSchema->getTypeMap();
119
120 2
        $breakingChanges = [];
121 2
        foreach ($oldTypeMap as $typeName => $oldType) {
122 2
            if (! isset($newTypeMap[$typeName])) {
123 1
                continue;
124
            }
125 2
            $newType = $newTypeMap[$typeName];
126 2
            if ($oldType instanceof $newType) {
127 2
                continue;
128
            }
129
130 2
            $oldTypeKindName   = self::typeKindName($oldType);
131 2
            $newTypeKindName   = self::typeKindName($newType);
132 2
            $breakingChanges[] = [
133 2
                'type'        => self::BREAKING_CHANGE_TYPE_CHANGED_KIND,
134 2
                'description' => "${typeName} changed from ${oldTypeKindName} to ${newTypeKindName}.",
135
            ];
136
        }
137
138 2
        return $breakingChanges;
139
    }
140
141
    /**
142
     * @return string
143
     *
144
     * @throws \TypeError
145
     */
146
    private static function typeKindName(Type $type)
147
    {
148 2
        if ($type instanceof ScalarType) {
149
            return 'a Scalar type';
150
        }
151
152 2
        if ($type instanceof ObjectType) {
153 1
            return 'an Object type';
154
        }
155
156 2
        if ($type instanceof InterfaceType) {
157 2
            return 'an Interface type';
158
        }
159
160 1
        if ($type instanceof UnionType) {
161 1
            return 'a Union type';
162
        }
163
164
        if ($type instanceof EnumType) {
165
            return 'an Enum type';
166
        }
167
168
        if ($type instanceof InputObjectType) {
169
            return 'an Input type';
170
        }
171
172
        throw new \TypeError('unknown type ' . $type->name);
173
    }
174
175
    /**
176
     * @return string[][]
177
     */
178
    public static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes(
179
        Schema $oldSchema,
180
        Schema $newSchema
181
    ) {
182 2
        $oldTypeMap = $oldSchema->getTypeMap();
183 2
        $newTypeMap = $newSchema->getTypeMap();
184
185 2
        $breakingChanges = [];
186 2
        foreach ($oldTypeMap as $typeName => $oldType) {
187 2
            $newType = $newTypeMap[$typeName] ?? null;
188 2
            if (! ($oldType instanceof ObjectType || $oldType instanceof InterfaceType) ||
189 2
                ! ($newType instanceof ObjectType || $newType instanceof InterfaceType) ||
190 2
                ! ($newType instanceof $oldType)
191
            ) {
192 2
                continue;
193
            }
194
195 2
            $oldTypeFieldsDef = $oldType->getFields();
196 2
            $newTypeFieldsDef = $newType->getFields();
197 2
            foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) {
198
                // Check if the field is missing on the type in the new schema.
199 2
                if (! isset($newTypeFieldsDef[$fieldName])) {
200 2
                    $breakingChanges[] = [
201 2
                        'type'        => self::BREAKING_CHANGE_FIELD_REMOVED,
202 2
                        'description' => "${typeName}.${fieldName} was removed.",
203
                    ];
204
                } else {
205 2
                    $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
206 2
                    $newFieldType = $newTypeFieldsDef[$fieldName]->getType();
207 2
                    $isSafe       = self::isChangeSafeForObjectOrInterfaceField(
208 2
                        $oldFieldType,
209 2
                        $newFieldType
210
                    );
211 2
                    if (! $isSafe) {
212 2
                        $oldFieldTypeString = $oldFieldType instanceof NamedType
213 2
                            ? $oldFieldType->name
214 2
                            : $oldFieldType;
215 2
                        $newFieldTypeString = $newFieldType instanceof NamedType
216 2
                            ? $newFieldType->name
217 2
                            : $newFieldType;
218 2
                        $breakingChanges[]  = [
219 2
                            'type'        => self::BREAKING_CHANGE_FIELD_CHANGED_KIND,
220 2
                            'description' => "${typeName}.${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}.",
221
                        ];
222
                    }
223
                }
224
            }
225
        }
226
227 2
        return $breakingChanges;
228
    }
229
230
    /**
231
     * @return bool
232
     */
233
    private static function isChangeSafeForObjectOrInterfaceField(
234
        Type $oldType,
235
        Type $newType
236
    ) {
237 2
        if ($oldType instanceof NamedType) {
238
            return // if they're both named types, see if their names are equivalent
239 2
                ($newType instanceof NamedType && $oldType->name === $newType->name) ||
240
                // moving from nullable to non-null of the same underlying type is safe
241 2
                ($newType instanceof NonNull &&
242 2
                    self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType())
243
                );
244
        }
245
246 2
        if ($oldType instanceof ListOfType) {
247
            return // if they're both lists, make sure the underlying types are compatible
248 2
                ($newType instanceof ListOfType &&
249 2
                    self::isChangeSafeForObjectOrInterfaceField(
250 2
                        $oldType->getWrappedType(),
251 2
                        $newType->getWrappedType()
252
                    )) ||
253
                // moving from nullable to non-null of the same underlying type is safe
254 1
                ($newType instanceof NonNull &&
255 2
                    self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType()));
256
        }
257
258 2
        if ($oldType instanceof NonNull) {
259
            // if they're both non-null, make sure the underlying types are compatible
260 2
            return $newType instanceof NonNull &&
261 2
                self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType());
262
        }
263
264
        return false;
265
    }
266
267
    /**
268
     * @return string[][]
269
     */
270
    public static function findFieldsThatChangedTypeOnInputObjectTypes(
271
        Schema $oldSchema,
272
        Schema $newSchema
273
    ) {
274 5
        $oldTypeMap = $oldSchema->getTypeMap();
275 5
        $newTypeMap = $newSchema->getTypeMap();
276
277 5
        $breakingChanges  = [];
278 5
        $dangerousChanges = [];
279 5
        foreach ($oldTypeMap as $typeName => $oldType) {
280 5
            $newType = $newTypeMap[$typeName] ?? null;
281 5
            if (! ($oldType instanceof InputObjectType) || ! ($newType instanceof InputObjectType)) {
282 5
                continue;
283
            }
284
285 3
            $oldTypeFieldsDef = $oldType->getFields();
286 3
            $newTypeFieldsDef = $newType->getFields();
287 3
            foreach (array_keys($oldTypeFieldsDef) as $fieldName) {
288 3
                if (! isset($newTypeFieldsDef[$fieldName])) {
289 1
                    $breakingChanges[] = [
290 1
                        'type'        => self::BREAKING_CHANGE_FIELD_REMOVED,
291 1
                        'description' => "${typeName}.${fieldName} was removed.",
292
                    ];
293
                } else {
294 3
                    $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
295 3
                    $newFieldType = $newTypeFieldsDef[$fieldName]->getType();
296
297 3
                    $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg(
298 3
                        $oldFieldType,
0 ignored issues
show
Bug introduced by
It seems like $oldFieldType can also be of type callable; however, parameter $oldType of GraphQL\Utils\BreakingCh...ObjectFieldOrFieldArg() does only seem to accept GraphQL\Type\Definition\Type, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

298
                        /** @scrutinizer ignore-type */ $oldFieldType,
Loading history...
299 3
                        $newFieldType
0 ignored issues
show
Bug introduced by
It seems like $newFieldType can also be of type callable; however, parameter $newType of GraphQL\Utils\BreakingCh...ObjectFieldOrFieldArg() does only seem to accept GraphQL\Type\Definition\Type, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

299
                        /** @scrutinizer ignore-type */ $newFieldType
Loading history...
300
                    );
301 3
                    if (! $isSafe) {
302 1
                        $oldFieldTypeString = $oldFieldType instanceof NamedType
303 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...
Bug introduced by
Accessing name on the interface GraphQL\Type\Definition\InputType suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
304 1
                            : $oldFieldType;
305 1
                        $newFieldTypeString = $newFieldType instanceof NamedType
306 1
                            ? $newFieldType->name
307 1
                            : $newFieldType;
308 1
                        $breakingChanges[]  = [
309 1
                            'type'        => self::BREAKING_CHANGE_FIELD_CHANGED_KIND,
310 3
                            'description' => "${typeName}.${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}.",
311
                        ];
312
                    }
313
                }
314
            }
315
            // Check if a field was added to the input object type
316 3
            foreach ($newTypeFieldsDef as $fieldName => $fieldDef) {
317 3
                if (isset($oldTypeFieldsDef[$fieldName])) {
318 3
                    continue;
319
                }
320
321 2
                $newTypeName = $newType->name;
322 2
                if ($fieldDef->getType() instanceof NonNull) {
323 1
                    $breakingChanges[] = [
324 1
                        'type'        => self::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED,
325 1
                        'description' => "A non-null field ${fieldName} on input type ${newTypeName} was added.",
326
                    ];
327
                } else {
328 2
                    $dangerousChanges[] = [
329 2
                        'type'        => self::DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED,
330 3
                        'description' => "A nullable field ${fieldName} on input type ${newTypeName} was added.",
331
                    ];
332
                }
333
            }
334
        }
335
336
        return [
337 5
            'breakingChanges'  => $breakingChanges,
338 5
            'dangerousChanges' => $dangerousChanges,
339
        ];
340
    }
341
342
    /**
343
     *
344
     * @return bool
345
     */
346
    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
    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
    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
    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
                        function ($arg) use ($oldArgDef) {
496 9
                            return $arg->name === $oldArgDef->name;
497 9
                        }
498
                    );
499 9
                    if ($newArgDef) {
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
                            function ($arg) use ($newArgDef) {
535 9
                                return $arg->name === $newArgDef->name;
536 9
                            }
537
                        );
538
539 9
                        if ($oldArgDef) {
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
    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
                if (Utils::find(
588 2
                    $newInterfaces,
589
                    function (InterfaceType $interface) use ($oldInterface) {
590
                        return $interface->name === $oldInterface->name;
591 2
                    }
592
                )) {
593
                    continue;
594
                }
595
596 2
                $breakingChanges[] = [
597 2
                    'type'        => self::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT,
598 2
                    'description' => sprintf('%s no longer implements interface %s.', $typeName, $oldInterface->name),
599
                ];
600
            }
601
        }
602
603 2
        return $breakingChanges;
604
    }
605
606
    /**
607
     * @return string[][]
608
     */
609
    public static function findRemovedDirectives(Schema $oldSchema, Schema $newSchema)
610
    {
611 3
        $removedDirectives = [];
612
613 3
        $newSchemaDirectiveMap = self::getDirectiveMapForSchema($newSchema);
614 3
        foreach ($oldSchema->getDirectives() as $directive) {
615 3
            if (isset($newSchemaDirectiveMap[$directive->name])) {
616 3
                continue;
617
            }
618
619 3
            $removedDirectives[] = [
620 3
                'type'        => self::BREAKING_CHANGE_DIRECTIVE_REMOVED,
621 3
                'description' => sprintf('%s was removed', $directive->name),
622
            ];
623
        }
624
625 3
        return $removedDirectives;
626
    }
627
628
    private static function getDirectiveMapForSchema(Schema $schema)
629
    {
630 6
        return Utils::keyMap(
631 6
            $schema->getDirectives(),
632
            function ($dir) {
633 6
                return $dir->name;
634 6
            }
635
        );
636
    }
637
638
    public static function findRemovedDirectiveArgs(Schema $oldSchema, Schema $newSchema)
639
    {
640 2
        $removedDirectiveArgs  = [];
641 2
        $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
642
643 2
        foreach ($newSchema->getDirectives() as $newDirective) {
644 2
            if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
645
                continue;
646
            }
647
648 2
            foreach (self::findRemovedArgsForDirectives(
649 2
                $oldSchemaDirectiveMap[$newDirective->name],
650 2
                $newDirective
651
            ) as $arg) {
652 2
                $removedDirectiveArgs[] = [
653 2
                    'type'        => self::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED,
654 2
                    'description' => sprintf('%s was removed from %s', $arg->name, $newDirective->name),
655
                ];
656
            }
657
        }
658
659 2
        return $removedDirectiveArgs;
660
    }
661
662
    public static function findRemovedArgsForDirectives(Directive $oldDirective, Directive $newDirective)
663
    {
664 2
        $removedArgs = [];
665 2
        $newArgMap   = self::getArgumentMapForDirective($newDirective);
666 2
        foreach ((array) $oldDirective->args as $arg) {
667 2
            if (isset($newArgMap[$arg->name])) {
668
                continue;
669
            }
670
671 2
            $removedArgs[] = $arg;
672
        }
673
674 2
        return $removedArgs;
675
    }
676
677
    private static function getArgumentMapForDirective(Directive $directive)
678
    {
679 3
        return Utils::keyMap(
680 3
            $directive->args ?: [],
681
            function ($arg) {
682 1
                return $arg->name;
683 3
            }
684
        );
685
    }
686
687
    public static function findAddedNonNullDirectiveArgs(Schema $oldSchema, Schema $newSchema)
688
    {
689 2
        $addedNonNullableArgs  = [];
690 2
        $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
691
692 2
        foreach ($newSchema->getDirectives() as $newDirective) {
693 2
            if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
694
                continue;
695
            }
696
697 2
            foreach (self::findAddedArgsForDirective(
698 2
                $oldSchemaDirectiveMap[$newDirective->name],
699 2
                $newDirective
700
            ) as $arg) {
701 2
                if (! $arg->getType() instanceof NonNull) {
702
                    continue;
703
                }
704 2
                $addedNonNullableArgs[] = [
705 2
                    'type'        => self::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED,
706 2
                    'description' => sprintf(
707 2
                        'A non-null arg %s on directive %s was added',
708 2
                        $arg->name,
709 2
                        $newDirective->name
710
                    ),
711
                ];
712
            }
713
        }
714
715 2
        return $addedNonNullableArgs;
716
    }
717
718
    /**
719
     * @return FieldArgument[]
720
     */
721
    public static function findAddedArgsForDirective(Directive $oldDirective, Directive $newDirective)
722
    {
723 2
        $addedArgs = [];
724 2
        $oldArgMap = self::getArgumentMapForDirective($oldDirective);
725 2
        foreach ((array) $newDirective->args as $arg) {
726 2
            if (isset($oldArgMap[$arg->name])) {
727
                continue;
728
            }
729
730 2
            $addedArgs[] = $arg;
731
        }
732
733 2
        return $addedArgs;
734
    }
735
736
    /**
737
     * @return string[][]
738
     */
739
    public static function findRemovedDirectiveLocations(Schema $oldSchema, Schema $newSchema)
740
    {
741 2
        $removedLocations      = [];
742 2
        $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
743
744 2
        foreach ($newSchema->getDirectives() as $newDirective) {
745 2
            if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
746
                continue;
747
            }
748
749 2
            foreach (self::findRemovedLocationsForDirective(
750 2
                $oldSchemaDirectiveMap[$newDirective->name],
751 2
                $newDirective
752
            ) as $location) {
753 2
                $removedLocations[] = [
754 2
                    'type'        => self::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED,
755 2
                    'description' => sprintf('%s was removed from %s', $location, $newDirective->name),
756
                ];
757
            }
758
        }
759
760 2
        return $removedLocations;
761
    }
762
763
    public static function findRemovedLocationsForDirective(Directive $oldDirective, Directive $newDirective)
764
    {
765 3
        $removedLocations = [];
766 3
        $newLocationSet   = array_flip($newDirective->locations);
767 3
        foreach ($oldDirective->locations as $oldLocation) {
768 3
            if (array_key_exists($oldLocation, $newLocationSet)) {
769 3
                continue;
770
            }
771
772 3
            $removedLocations[] = $oldLocation;
773
        }
774
775 3
        return $removedLocations;
776
    }
777
778
    /**
779
     * Given two schemas, returns an Array containing descriptions of all the types
780
     * of potentially dangerous changes covered by the other functions down below.
781
     *
782
     * @return string[][]
783
     */
784
    public static function findDangerousChanges(Schema $oldSchema, Schema $newSchema)
785
    {
786 1
        return array_merge(
787 1
            self::findArgChanges($oldSchema, $newSchema)['dangerousChanges'],
788 1
            self::findValuesAddedToEnums($oldSchema, $newSchema),
789 1
            self::findInterfacesAddedToObjectTypes($oldSchema, $newSchema),
790 1
            self::findTypesAddedToUnions($oldSchema, $newSchema),
791 1
            self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['dangerousChanges']
792
        );
793
    }
794
795
    /**
796
     * Given two schemas, returns an Array containing descriptions of any dangerous
797
     * changes in the newSchema related to adding values to an enum type.
798
     *
799
     * @return string[][]
800
     */
801
    public static function findValuesAddedToEnums(
802
        Schema $oldSchema,
803
        Schema $newSchema
804
    ) {
805 2
        $oldTypeMap = $oldSchema->getTypeMap();
806 2
        $newTypeMap = $newSchema->getTypeMap();
807
808 2
        $valuesAddedToEnums = [];
809 2
        foreach ($oldTypeMap as $typeName => $oldType) {
810 2
            $newType = $newTypeMap[$typeName] ?? null;
811 2
            if (! ($oldType instanceof EnumType) || ! ($newType instanceof EnumType)) {
812 2
                continue;
813
            }
814 2
            $valuesInOldEnum = [];
815 2
            foreach ($oldType->getValues() as $value) {
816 2
                $valuesInOldEnum[$value->name] = true;
817
            }
818 2
            foreach ($newType->getValues() as $value) {
819 2
                if (isset($valuesInOldEnum[$value->name])) {
820 2
                    continue;
821
                }
822
823 2
                $valuesAddedToEnums[] = [
824 2
                    'type'        => self::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM,
825 2
                    'description' => sprintf('%s was added to enum type %s.', $value->name, $typeName),
826
                ];
827
            }
828
        }
829
830 2
        return $valuesAddedToEnums;
831
    }
832
833
    /**
834
     *
835
     * @return string[][]
836
     */
837
    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
                if (Utils::find(
855 2
                    $oldInterfaces,
856
                    function (InterfaceType $interface) use ($newInterface) {
857
                        return $interface->name === $newInterface->name;
858 2
                    }
859
                )) {
860
                    continue;
861
                }
862
863 2
                $interfacesAddedToObjectTypes[] = [
864 2
                    'type'        => self::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT,
865 2
                    'description' => sprintf(
866 2
                        '%s added to interfaces implemented by %s.',
867 2
                        $newInterface->name,
868 2
                        $typeName
869
                    ),
870
                ];
871
            }
872
        }
873
874 2
        return $interfacesAddedToObjectTypes;
875
    }
876
877
    /**
878
     * Given two schemas, returns an Array containing descriptions of any dangerous
879
     * changes in the newSchema related to adding types to a union type.
880
     *
881
     * @return string[][]
882
     */
883
    public static function findTypesAddedToUnions(
884
        Schema $oldSchema,
885
        Schema $newSchema
886
    ) {
887 2
        $oldTypeMap = $oldSchema->getTypeMap();
888 2
        $newTypeMap = $newSchema->getTypeMap();
889
890 2
        $typesAddedToUnion = [];
891 2
        foreach ($newTypeMap as $typeName => $newType) {
892 2
            $oldType = $oldTypeMap[$typeName] ?? null;
893 2
            if (! ($oldType instanceof UnionType) || ! ($newType instanceof UnionType)) {
894 2
                continue;
895
            }
896
897 2
            $typeNamesInOldUnion = [];
898 2
            foreach ($oldType->getTypes() as $type) {
899 2
                $typeNamesInOldUnion[$type->name] = true;
900
            }
901 2
            foreach ($newType->getTypes() as $type) {
902 2
                if (isset($typeNamesInOldUnion[$type->name])) {
903 2
                    continue;
904
                }
905
906 2
                $typesAddedToUnion[] = [
907 2
                    'type'        => self::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION,
908 2
                    'description' => sprintf('%s was added to union type %s.', $type->name, $typeName),
909
                ];
910
            }
911
        }
912
913 2
        return $typesAddedToUnion;
914
    }
915
}
916
917
class_alias(BreakingChangesFinder::class, 'GraphQL\Utils\FindBreakingChanges');
918