findRemovedArgsForDirectives()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3.0175

Importance

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