Completed
Push — master ( c9c830...8d8462 )
by Vladimir
89:58 queued 86:21
created

getArgumentMapForDirective()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 1
nop 1
crap 2
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
                        if ($oldFieldType instanceof NamedType) {
308
                            /** @var ScalarType|ObjectType|InterfaceType|UnionType|EnumType|InputObjectType $oldFieldType */
309 1
                            $oldFieldTypeString = $oldFieldType->name;
310
                        } else {
311 1
                            $oldFieldTypeString = $oldFieldType;
312
                        }
313 1
                        if ($newFieldType instanceof NamedType) {
314
                            /** @var ScalarType|ObjectType|InterfaceType|UnionType|EnumType|InputObjectType $newFieldType */
315 1
                            $newFieldTypeString = $newFieldType->name;
316
                        } else {
317 1
                            $newFieldTypeString = $newFieldType;
318
                        }
319 1
                        $breakingChanges[] = [
320 1
                            'type'        => self::BREAKING_CHANGE_FIELD_CHANGED_KIND,
321 3
                            'description' => "${typeName}.${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}.",
322
                        ];
323
                    }
324
                }
325
            }
326
            // Check if a field was added to the input object type
327 3
            foreach ($newTypeFieldsDef as $fieldName => $fieldDef) {
328 3
                if (isset($oldTypeFieldsDef[$fieldName])) {
329 3
                    continue;
330
                }
331
332 2
                $newTypeName = $newType->name;
333 2
                if ($fieldDef->getType() instanceof NonNull) {
334 1
                    $breakingChanges[] = [
335 1
                        'type'        => self::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED,
336 1
                        'description' => "A non-null field ${fieldName} on input type ${newTypeName} was added.",
337
                    ];
338
                } else {
339 2
                    $dangerousChanges[] = [
340 2
                        'type'        => self::DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED,
341 3
                        'description' => "A nullable field ${fieldName} on input type ${newTypeName} was added.",
342
                    ];
343
                }
344
            }
345
        }
346
347
        return [
348 5
            'breakingChanges'  => $breakingChanges,
349 5
            'dangerousChanges' => $dangerousChanges,
350
        ];
351
    }
352
353
    /**
354
     * @return bool
355
     */
356 12
    private static function isChangeSafeForInputObjectFieldOrFieldArg(
357
        Type $oldType,
358
        Type $newType
359
    ) {
360 12
        if ($oldType instanceof NamedType) {
361 12
            if (! $newType instanceof NamedType) {
362 2
                return false;
363
            }
364
365
            // if they're both named types, see if their names are equivalent
366 12
            return $oldType->name === $newType->name;
367
        }
368
369 4
        if ($oldType instanceof ListOfType) {
370
            // if they're both lists, make sure the underlying types are compatible
371 2
            return $newType instanceof ListOfType &&
372 2
                self::isChangeSafeForInputObjectFieldOrFieldArg(
373 2
                    $oldType->getWrappedType(),
374 2
                    $newType->getWrappedType()
375
                );
376
        }
377
378 4
        if ($oldType instanceof NonNull) {
379
            return // if they're both non-null, make sure the underlying types are
380
                // compatible
381 4
                ($newType instanceof NonNull &&
382 3
                    self::isChangeSafeForInputObjectFieldOrFieldArg(
383 3
                        $oldType->getWrappedType(),
384 3
                        $newType->getWrappedType()
385
                    )) ||
386
                // moving from non-null to nullable of the same underlying type is safe
387 3
                ! ($newType instanceof NonNull) &&
388 4
                self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType);
389
        }
390
391
        return false;
392
    }
393
394
    /**
395
     * Given two schemas, returns an Array containing descriptions of any breaking
396
     * changes in the newSchema related to removing types from a union type.
397
     *
398
     * @return string[][]
399
     */
400 2
    public static function findTypesRemovedFromUnions(
401
        Schema $oldSchema,
402
        Schema $newSchema
403
    ) {
404 2
        $oldTypeMap = $oldSchema->getTypeMap();
405 2
        $newTypeMap = $newSchema->getTypeMap();
406
407 2
        $typesRemovedFromUnion = [];
408 2
        foreach ($oldTypeMap as $typeName => $oldType) {
409 2
            $newType = $newTypeMap[$typeName] ?? null;
410 2
            if (! ($oldType instanceof UnionType) || ! ($newType instanceof UnionType)) {
411 2
                continue;
412
            }
413 2
            $typeNamesInNewUnion = [];
414 2
            foreach ($newType->getTypes() as $type) {
415 2
                $typeNamesInNewUnion[$type->name] = true;
416
            }
417 2
            foreach ($oldType->getTypes() as $type) {
418 2
                if (isset($typeNamesInNewUnion[$type->name])) {
419 2
                    continue;
420
                }
421
422 2
                $typesRemovedFromUnion[] = [
423 2
                    'type'        => self::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION,
424 2
                    'description' => sprintf('%s was removed from union type %s.', $type->name, $typeName),
425
                ];
426
            }
427
        }
428
429 2
        return $typesRemovedFromUnion;
430
    }
431
432
    /**
433
     * Given two schemas, returns an Array containing descriptions of any breaking
434
     * changes in the newSchema related to removing values from an enum type.
435
     *
436
     * @return string[][]
437
     */
438 2
    public static function findValuesRemovedFromEnums(
439
        Schema $oldSchema,
440
        Schema $newSchema
441
    ) {
442 2
        $oldTypeMap = $oldSchema->getTypeMap();
443 2
        $newTypeMap = $newSchema->getTypeMap();
444
445 2
        $valuesRemovedFromEnums = [];
446 2
        foreach ($oldTypeMap as $typeName => $oldType) {
447 2
            $newType = $newTypeMap[$typeName] ?? null;
448 2
            if (! ($oldType instanceof EnumType) || ! ($newType instanceof EnumType)) {
449 2
                continue;
450
            }
451 2
            $valuesInNewEnum = [];
452 2
            foreach ($newType->getValues() as $value) {
453 2
                $valuesInNewEnum[$value->name] = true;
454
            }
455 2
            foreach ($oldType->getValues() as $value) {
456 2
                if (isset($valuesInNewEnum[$value->name])) {
457 2
                    continue;
458
                }
459
460 2
                $valuesRemovedFromEnums[] = [
461 2
                    'type'        => self::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM,
462 2
                    'description' => sprintf('%s was removed from enum type %s.', $value->name, $typeName),
463
                ];
464
            }
465
        }
466
467 2
        return $valuesRemovedFromEnums;
468
    }
469
470
    /**
471
     * Given two schemas, returns an Array containing descriptions of any
472
     * breaking or dangerous changes in the newSchema related to arguments
473
     * (such as removal or change of type of an argument, or a change in an
474
     * argument's default value).
475
     *
476
     * @return string[][]
477
     */
478 9
    public static function findArgChanges(
479
        Schema $oldSchema,
480
        Schema $newSchema
481
    ) {
482 9
        $oldTypeMap = $oldSchema->getTypeMap();
483 9
        $newTypeMap = $newSchema->getTypeMap();
484
485 9
        $breakingChanges  = [];
486 9
        $dangerousChanges = [];
487
488 9
        foreach ($oldTypeMap as $typeName => $oldType) {
489 9
            $newType = $newTypeMap[$typeName] ?? null;
490 9
            if (! ($oldType instanceof ObjectType || $oldType instanceof InterfaceType) ||
491 9
                ! ($newType instanceof ObjectType || $newType instanceof InterfaceType) ||
492 9
                ! ($newType instanceof $oldType)
493
            ) {
494 9
                continue;
495
            }
496
497 9
            $oldTypeFields = $oldType->getFields();
498 9
            $newTypeFields = $newType->getFields();
499
500 9
            foreach ($oldTypeFields as $fieldName => $oldField) {
501 9
                if (! isset($newTypeFields[$fieldName])) {
502 1
                    continue;
503
                }
504
505 9
                foreach ($oldField->args as $oldArgDef) {
506 9
                    $newArgs   = $newTypeFields[$fieldName]->args;
507 9
                    $newArgDef = Utils::find(
508 9
                        $newArgs,
509
                        static function ($arg) use ($oldArgDef) {
510 9
                            return $arg->name === $oldArgDef->name;
511 9
                        }
512
                    );
513 9
                    if ($newArgDef !== null) {
514 9
                        $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg(
515 9
                            $oldArgDef->getType(),
516 9
                            $newArgDef->getType()
517
                        );
518
                        /** @var ScalarType|EnumType|InputObjectType|ListOfType|NonNull $oldArgType */
519 9
                        $oldArgType = $oldArgDef->getType();
520 9
                        $oldArgName = $oldArgDef->name;
521 9
                        if (! $isSafe) {
522 2
                            $newArgType        = $newArgDef->getType();
523 2
                            $breakingChanges[] = [
524 2
                                'type'        => self::BREAKING_CHANGE_ARG_CHANGED_KIND,
525 2
                                'description' => "${typeName}.${fieldName} arg ${oldArgName} has changed type from ${oldArgType} to ${newArgType}",
526
                            ];
527 9
                        } elseif ($oldArgDef->defaultValueExists() && $oldArgDef->defaultValue !== $newArgDef->defaultValue) {
528 2
                            $dangerousChanges[] = [
529 2
                                'type'        => self::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED,
530 9
                                'description' => "${typeName}.${fieldName} arg ${oldArgName} has changed defaultValue",
531
                            ];
532
                        }
533
                    } else {
534 1
                        $breakingChanges[] = [
535 1
                            'type'        => self::BREAKING_CHANGE_ARG_REMOVED,
536 1
                            'description' => sprintf(
537 1
                                '%s.%s arg %s was removed',
538 1
                                $typeName,
539 1
                                $fieldName,
540 1
                                $oldArgDef->name
541
                            ),
542
                        ];
543
                    }
544
                    // Check if a non-null arg was added to the field
545 9
                    foreach ($newTypeFields[$fieldName]->args as $newTypeFieldArgDef) {
546 9
                        $oldArgs   = $oldTypeFields[$fieldName]->args;
547 9
                        $oldArgDef = Utils::find(
548 9
                            $oldArgs,
549
                            static function ($arg) use ($newTypeFieldArgDef) {
550 9
                                return $arg->name === $newTypeFieldArgDef->name;
551 9
                            }
552
                        );
553
554 9
                        if ($oldArgDef !== null) {
555 9
                            continue;
556
                        }
557
558 2
                        $newTypeName = $newType->name;
559 2
                        $newArgName  = $newTypeFieldArgDef->name;
560 2
                        if ($newTypeFieldArgDef->getType() instanceof NonNull) {
561 1
                            $breakingChanges[] = [
562 1
                                'type'        => self::BREAKING_CHANGE_NON_NULL_ARG_ADDED,
563 1
                                'description' => "A non-null arg ${newArgName} on ${newTypeName}.${fieldName} was added",
564
                            ];
565
                        } else {
566 2
                            $dangerousChanges[] = [
567 2
                                'type'        => self::DANGEROUS_CHANGE_NULLABLE_ARG_ADDED,
568 9
                                'description' => "A nullable arg ${newArgName} on ${newTypeName}.${fieldName} was added",
569
                            ];
570
                        }
571
                    }
572
                }
573
            }
574
        }
575
576
        return [
577 9
            'breakingChanges'  => $breakingChanges,
578 9
            'dangerousChanges' => $dangerousChanges,
579
        ];
580
    }
581
582
    /**
583
     * @return string[][]
584
     */
585 2
    public static function findInterfacesRemovedFromObjectTypes(
586
        Schema $oldSchema,
587
        Schema $newSchema
588
    ) {
589 2
        $oldTypeMap      = $oldSchema->getTypeMap();
590 2
        $newTypeMap      = $newSchema->getTypeMap();
591 2
        $breakingChanges = [];
592
593 2
        foreach ($oldTypeMap as $typeName => $oldType) {
594 2
            $newType = $newTypeMap[$typeName] ?? null;
595 2
            if (! ($oldType instanceof ObjectType) || ! ($newType instanceof ObjectType)) {
596 2
                continue;
597
            }
598
599 2
            $oldInterfaces = $oldType->getInterfaces();
600 2
            $newInterfaces = $newType->getInterfaces();
601 2
            foreach ($oldInterfaces as $oldInterface) {
602 2
                $interface = Utils::find(
603 2
                    $newInterfaces,
604
                    static function (InterfaceType $interface) use ($oldInterface) : bool {
605
                        return $interface->name === $oldInterface->name;
606 2
                    }
607
                );
608 2
                if ($interface !== null) {
609
                    continue;
610
                }
611
612 2
                $breakingChanges[] = [
613 2
                    'type'        => self::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT,
614 2
                    'description' => sprintf('%s no longer implements interface %s.', $typeName, $oldInterface->name),
615
                ];
616
            }
617
        }
618
619 2
        return $breakingChanges;
620
    }
621
622
    /**
623
     * @return string[][]
624
     */
625 3
    public static function findRemovedDirectives(Schema $oldSchema, Schema $newSchema)
626
    {
627 3
        $removedDirectives = [];
628
629 3
        $newSchemaDirectiveMap = self::getDirectiveMapForSchema($newSchema);
630 3
        foreach ($oldSchema->getDirectives() as $directive) {
631 3
            if (isset($newSchemaDirectiveMap[$directive->name])) {
632 3
                continue;
633
            }
634
635 3
            $removedDirectives[] = [
636 3
                'type'        => self::BREAKING_CHANGE_DIRECTIVE_REMOVED,
637 3
                'description' => sprintf('%s was removed', $directive->name),
638
            ];
639
        }
640
641 3
        return $removedDirectives;
642
    }
643
644 6
    private static function getDirectiveMapForSchema(Schema $schema)
645
    {
646 6
        return Utils::keyMap(
647 6
            $schema->getDirectives(),
648
            static function ($dir) {
649 6
                return $dir->name;
650 6
            }
651
        );
652
    }
653
654 2
    public static function findRemovedDirectiveArgs(Schema $oldSchema, Schema $newSchema)
655
    {
656 2
        $removedDirectiveArgs  = [];
657 2
        $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
658
659 2
        foreach ($newSchema->getDirectives() as $newDirective) {
660 2
            if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
661
                continue;
662
            }
663
664 2
            foreach (self::findRemovedArgsForDirectives(
665 2
                $oldSchemaDirectiveMap[$newDirective->name],
666 2
                $newDirective
667
            ) as $arg) {
668 2
                $removedDirectiveArgs[] = [
669 2
                    'type'        => self::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED,
670 2
                    'description' => sprintf('%s was removed from %s', $arg->name, $newDirective->name),
671
                ];
672
            }
673
        }
674
675 2
        return $removedDirectiveArgs;
676
    }
677
678 2
    public static function findRemovedArgsForDirectives(Directive $oldDirective, Directive $newDirective)
679
    {
680 2
        $removedArgs = [];
681 2
        $newArgMap   = self::getArgumentMapForDirective($newDirective);
682 2
        foreach ($oldDirective->args as $arg) {
683 2
            if (isset($newArgMap[$arg->name])) {
684
                continue;
685
            }
686
687 2
            $removedArgs[] = $arg;
688
        }
689
690 2
        return $removedArgs;
691
    }
692
693 3
    private static function getArgumentMapForDirective(Directive $directive)
694
    {
695 3
        return Utils::keyMap(
696 3
            $directive->args ?: [],
697
            static function ($arg) {
698 1
                return $arg->name;
699 3
            }
700
        );
701
    }
702
703 2
    public static function findAddedNonNullDirectiveArgs(Schema $oldSchema, Schema $newSchema)
704
    {
705 2
        $addedNonNullableArgs  = [];
706 2
        $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
707
708 2
        foreach ($newSchema->getDirectives() as $newDirective) {
709 2
            if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
710
                continue;
711
            }
712
713 2
            foreach (self::findAddedArgsForDirective(
714 2
                $oldSchemaDirectiveMap[$newDirective->name],
715 2
                $newDirective
716
            ) as $arg) {
717 2
                if (! $arg->getType() instanceof NonNull) {
718
                    continue;
719
                }
720 2
                $addedNonNullableArgs[] = [
721 2
                    'type'        => self::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED,
722 2
                    'description' => sprintf(
723 2
                        'A non-null arg %s on directive %s was added',
724 2
                        $arg->name,
725 2
                        $newDirective->name
726
                    ),
727
                ];
728
            }
729
        }
730
731 2
        return $addedNonNullableArgs;
732
    }
733
734
    /**
735
     * @return FieldArgument[]
736
     */
737 2
    public static function findAddedArgsForDirective(Directive $oldDirective, Directive $newDirective)
738
    {
739 2
        $addedArgs = [];
740 2
        $oldArgMap = self::getArgumentMapForDirective($oldDirective);
741 2
        foreach ($newDirective->args as $arg) {
742 2
            if (isset($oldArgMap[$arg->name])) {
743
                continue;
744
            }
745
746 2
            $addedArgs[] = $arg;
747
        }
748
749 2
        return $addedArgs;
750
    }
751
752
    /**
753
     * @return string[][]
754
     */
755 2
    public static function findRemovedDirectiveLocations(Schema $oldSchema, Schema $newSchema)
756
    {
757 2
        $removedLocations      = [];
758 2
        $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
759
760 2
        foreach ($newSchema->getDirectives() as $newDirective) {
761 2
            if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
762
                continue;
763
            }
764
765 2
            foreach (self::findRemovedLocationsForDirective(
766 2
                $oldSchemaDirectiveMap[$newDirective->name],
767 2
                $newDirective
768
            ) as $location) {
769 2
                $removedLocations[] = [
770 2
                    'type'        => self::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED,
771 2
                    'description' => sprintf('%s was removed from %s', $location, $newDirective->name),
772
                ];
773
            }
774
        }
775
776 2
        return $removedLocations;
777
    }
778
779 3
    public static function findRemovedLocationsForDirective(Directive $oldDirective, Directive $newDirective)
780
    {
781 3
        $removedLocations = [];
782 3
        $newLocationSet   = array_flip($newDirective->locations);
783 3
        foreach ($oldDirective->locations as $oldLocation) {
784 3
            if (array_key_exists($oldLocation, $newLocationSet)) {
785 3
                continue;
786
            }
787
788 3
            $removedLocations[] = $oldLocation;
789
        }
790
791 3
        return $removedLocations;
792
    }
793
794
    /**
795
     * Given two schemas, returns an Array containing descriptions of all the types
796
     * of potentially dangerous changes covered by the other functions down below.
797
     *
798
     * @return string[][]
799
     */
800 1
    public static function findDangerousChanges(Schema $oldSchema, Schema $newSchema)
801
    {
802 1
        return array_merge(
803 1
            self::findArgChanges($oldSchema, $newSchema)['dangerousChanges'],
804 1
            self::findValuesAddedToEnums($oldSchema, $newSchema),
805 1
            self::findInterfacesAddedToObjectTypes($oldSchema, $newSchema),
806 1
            self::findTypesAddedToUnions($oldSchema, $newSchema),
807 1
            self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['dangerousChanges']
808
        );
809
    }
810
811
    /**
812
     * Given two schemas, returns an Array containing descriptions of any dangerous
813
     * changes in the newSchema related to adding values to an enum type.
814
     *
815
     * @return string[][]
816
     */
817 2
    public static function findValuesAddedToEnums(
818
        Schema $oldSchema,
819
        Schema $newSchema
820
    ) {
821 2
        $oldTypeMap = $oldSchema->getTypeMap();
822 2
        $newTypeMap = $newSchema->getTypeMap();
823
824 2
        $valuesAddedToEnums = [];
825 2
        foreach ($oldTypeMap as $typeName => $oldType) {
826 2
            $newType = $newTypeMap[$typeName] ?? null;
827 2
            if (! ($oldType instanceof EnumType) || ! ($newType instanceof EnumType)) {
828 2
                continue;
829
            }
830 2
            $valuesInOldEnum = [];
831 2
            foreach ($oldType->getValues() as $value) {
832 2
                $valuesInOldEnum[$value->name] = true;
833
            }
834 2
            foreach ($newType->getValues() as $value) {
835 2
                if (isset($valuesInOldEnum[$value->name])) {
836 2
                    continue;
837
                }
838
839 2
                $valuesAddedToEnums[] = [
840 2
                    'type'        => self::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM,
841 2
                    'description' => sprintf('%s was added to enum type %s.', $value->name, $typeName),
842
                ];
843
            }
844
        }
845
846 2
        return $valuesAddedToEnums;
847
    }
848
849
    /**
850
     * @return string[][]
851
     */
852 2
    public static function findInterfacesAddedToObjectTypes(
853
        Schema $oldSchema,
854
        Schema $newSchema
855
    ) {
856 2
        $oldTypeMap                   = $oldSchema->getTypeMap();
857 2
        $newTypeMap                   = $newSchema->getTypeMap();
858 2
        $interfacesAddedToObjectTypes = [];
859
860 2
        foreach ($newTypeMap as $typeName => $newType) {
861 2
            $oldType = $oldTypeMap[$typeName] ?? null;
862 2
            if (! ($oldType instanceof ObjectType) || ! ($newType instanceof ObjectType)) {
863 2
                continue;
864
            }
865
866 2
            $oldInterfaces = $oldType->getInterfaces();
867 2
            $newInterfaces = $newType->getInterfaces();
868 2
            foreach ($newInterfaces as $newInterface) {
869 2
                $interface = Utils::find(
870 2
                    $oldInterfaces,
871
                    static function (InterfaceType $interface) use ($newInterface) : bool {
872
                        return $interface->name === $newInterface->name;
873 2
                    }
874
                );
875
876 2
                if ($interface !== null) {
877
                    continue;
878
                }
879
880 2
                $interfacesAddedToObjectTypes[] = [
881 2
                    'type'        => self::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT,
882 2
                    'description' => sprintf(
883 2
                        '%s added to interfaces implemented by %s.',
884 2
                        $newInterface->name,
885 2
                        $typeName
886
                    ),
887
                ];
888
            }
889
        }
890
891 2
        return $interfacesAddedToObjectTypes;
892
    }
893
894
    /**
895
     * Given two schemas, returns an Array containing descriptions of any dangerous
896
     * changes in the newSchema related to adding types to a union type.
897
     *
898
     * @return string[][]
899
     */
900 2
    public static function findTypesAddedToUnions(
901
        Schema $oldSchema,
902
        Schema $newSchema
903
    ) {
904 2
        $oldTypeMap = $oldSchema->getTypeMap();
905 2
        $newTypeMap = $newSchema->getTypeMap();
906
907 2
        $typesAddedToUnion = [];
908 2
        foreach ($newTypeMap as $typeName => $newType) {
909 2
            $oldType = $oldTypeMap[$typeName] ?? null;
910 2
            if (! ($oldType instanceof UnionType) || ! ($newType instanceof UnionType)) {
911 2
                continue;
912
            }
913
914 2
            $typeNamesInOldUnion = [];
915 2
            foreach ($oldType->getTypes() as $type) {
916 2
                $typeNamesInOldUnion[$type->name] = true;
917
            }
918 2
            foreach ($newType->getTypes() as $type) {
919 2
                if (isset($typeNamesInOldUnion[$type->name])) {
920 2
                    continue;
921
                }
922
923 2
                $typesAddedToUnion[] = [
924 2
                    'type'        => self::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION,
925 2
                    'description' => sprintf('%s was added to union type %s.', $type->name, $typeName),
926
                ];
927
            }
928
        }
929
930 2
        return $typesAddedToUnion;
931
    }
932
}
933
934
class_alias(BreakingChangesFinder::class, 'GraphQL\Utils\FindBreakingChanges');
935