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

Printer   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 448
Duplicated Lines 0 %

Test Coverage

Coverage 99.15%

Importance

Changes 1
Bugs 0 Features 1
Metric Value
wmc 33
eloc 226
c 1
b 0
f 1
dl 0
loc 448
rs 9.76
ccs 233
cts 235
cp 0.9915

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 2 1
A doPrint() 0 6 2
A length() 0 3 2
A wrap() 0 3 2
A join() 0 13 2
A manyList() 0 3 2
A indent() 0 3 2
A printBlockString() 0 7 5
A addDescription() 0 4 1
C printAST() 0 350 11
A block() 0 5 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Language;
6
7
use GraphQL\Language\AST\ArgumentNode;
8
use GraphQL\Language\AST\BooleanValueNode;
9
use GraphQL\Language\AST\DirectiveDefinitionNode;
10
use GraphQL\Language\AST\DirectiveNode;
11
use GraphQL\Language\AST\DocumentNode;
12
use GraphQL\Language\AST\EnumTypeDefinitionNode;
13
use GraphQL\Language\AST\EnumTypeExtensionNode;
14
use GraphQL\Language\AST\EnumValueDefinitionNode;
15
use GraphQL\Language\AST\EnumValueNode;
16
use GraphQL\Language\AST\FieldDefinitionNode;
17
use GraphQL\Language\AST\FieldNode;
18
use GraphQL\Language\AST\FloatValueNode;
19
use GraphQL\Language\AST\FragmentDefinitionNode;
20
use GraphQL\Language\AST\FragmentSpreadNode;
21
use GraphQL\Language\AST\InlineFragmentNode;
22
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
23
use GraphQL\Language\AST\InputObjectTypeExtensionNode;
24
use GraphQL\Language\AST\InputValueDefinitionNode;
25
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
26
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
27
use GraphQL\Language\AST\IntValueNode;
28
use GraphQL\Language\AST\ListTypeNode;
29
use GraphQL\Language\AST\ListValueNode;
30
use GraphQL\Language\AST\NamedTypeNode;
31
use GraphQL\Language\AST\NameNode;
32
use GraphQL\Language\AST\Node;
33
use GraphQL\Language\AST\NodeKind;
34
use GraphQL\Language\AST\NonNullTypeNode;
35
use GraphQL\Language\AST\NullValueNode;
36
use GraphQL\Language\AST\ObjectFieldNode;
37
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
38
use GraphQL\Language\AST\ObjectTypeExtensionNode;
39
use GraphQL\Language\AST\ObjectValueNode;
40
use GraphQL\Language\AST\OperationDefinitionNode;
41
use GraphQL\Language\AST\OperationTypeDefinitionNode;
42
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
43
use GraphQL\Language\AST\ScalarTypeExtensionNode;
44
use GraphQL\Language\AST\SchemaDefinitionNode;
45
use GraphQL\Language\AST\SchemaTypeExtensionNode;
46
use GraphQL\Language\AST\SelectionSetNode;
47
use GraphQL\Language\AST\StringValueNode;
48
use GraphQL\Language\AST\UnionTypeDefinitionNode;
49
use GraphQL\Language\AST\UnionTypeExtensionNode;
50
use GraphQL\Language\AST\VariableDefinitionNode;
51
use GraphQL\Language\AST\VariableNode;
52
use GraphQL\Utils\Utils;
53
use function count;
54
use function implode;
55
use function json_encode;
56
use function preg_replace;
57
use function sprintf;
58
use function str_replace;
59
use function strpos;
60
61
/**
62
 * Prints AST to string. Capable of printing GraphQL queries and Type definition language.
63
 * Useful for pretty-printing queries or printing back AST for logging, documentation, etc.
64
 *
65
 * Usage example:
66
 *
67
 * ```php
68
 * $query = 'query myQuery {someField}';
69
 * $ast = GraphQL\Language\Parser::parse($query);
70
 * $printed = GraphQL\Language\Printer::doPrint($ast);
71
 * ```
72
 */
73
class Printer
74
{
75
    /**
76
     * Prints AST to string. Capable of printing GraphQL queries and Type definition language.
77
     *
78
     * @param Node $ast
79
     *
80
     * @return string
81
     *
82
     * @api
83
     */
84 140
    public static function doPrint($ast)
85
    {
86 140
        static $instance;
87 140
        $instance = $instance ?: new static();
88
89 140
        return $instance->printAST($ast);
90
    }
91
92 1
    protected function __construct()
93
    {
94 1
    }
95
96 140
    public function printAST($ast)
97
    {
98 140
        return Visitor::visit(
99 140
            $ast,
100
            [
101
                'leave' => [
102
                    NodeKind::NAME => static function (NameNode $node) {
103 75
                        return '' . $node->value;
104 140
                    },
105
106
                    NodeKind::VARIABLE => static function (VariableNode $node) {
107 4
                        return '$' . $node->name;
108 140
                    },
109
110
                    NodeKind::DOCUMENT => function (DocumentNode $node) {
111 29
                        return $this->join($node->definitions, "\n\n") . "\n";
112 140
                    },
113
114
                    NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) {
115 8
                        $op           = $node->operation;
116 8
                        $name         = $node->name;
117 8
                        $varDefs      = $this->wrap('(', $this->join($node->variableDefinitions, ', '), ')');
118 8
                        $directives   = $this->join($node->directives, ' ');
119 8
                        $selectionSet = $node->selectionSet;
120
121
                        // Anonymous queries with no directives or variable definitions can use
122
                        // the query short form.
123 8
                        return ! $name && ! $directives && ! $varDefs && $op === 'query'
0 ignored issues
show
introduced by
$name is of type GraphQL\Language\AST\NameNode, thus it always evaluated to true.
Loading history...
124 8
                            ? $selectionSet
125 8
                            : $this->join([$op, $this->join([$name, $varDefs]), $directives, $selectionSet], ' ');
126 140
                    },
127
128
                    NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) {
129 4
                        return $node->variable . ': ' . $node->type . $this->wrap(' = ', $node->defaultValue);
0 ignored issues
show
Bug introduced by
Are you sure $node->type of type GraphQL\Language\AST\TypeNode can be used in concatenation? Consider adding a __toString()-method. ( Ignorable by Annotation )

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

129
                        return $node->variable . ': ' . /** @scrutinizer ignore-type */ $node->type . $this->wrap(' = ', $node->defaultValue);
Loading history...
130 140
                    },
131
132
                    NodeKind::SELECTION_SET => function (SelectionSetNode $node) {
133 9
                        return $this->block($node->selections);
134 140
                    },
135
136
                    NodeKind::FIELD => function (FieldNode $node) {
137 10
                        return $this->join(
138
                            [
139 10
                                $this->wrap('', $node->alias, ': ') . $node->name . $this->wrap(
140 10
                                    '(',
141 10
                                    $this->join($node->arguments, ', '),
142 10
                                    ')'
143
                                ),
144 10
                                $this->join($node->directives, ' '),
145 10
                                $node->selectionSet,
146
                            ],
147 10
                            ' '
148
                        );
149 140
                    },
150
151
                    NodeKind::ARGUMENT => static function (ArgumentNode $node) {
152 9
                        return $node->name . ': ' . $node->value;
0 ignored issues
show
Bug introduced by
Are you sure $node->value of type GraphQL\Language\AST\ValueNode can be used in concatenation? Consider adding a __toString()-method. ( Ignorable by Annotation )

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

152
                        return $node->name . ': ' . /** @scrutinizer ignore-type */ $node->value;
Loading history...
153 140
                    },
154
155
                    NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) {
156 2
                        return '...' . $node->name . $this->wrap(' ', $this->join($node->directives, ' '));
157 140
                    },
158
159
                    NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) {
160 2
                        return $this->join(
161
                            [
162 2
                                '...',
163 2
                                $this->wrap('on ', $node->typeCondition),
164 2
                                $this->join($node->directives, ' '),
165 2
                                $node->selectionSet,
166
                            ],
167 2
                            ' '
168
                        );
169 140
                    },
170
171
                    NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) {
172
                        // Note: fragment variable definitions are experimental and may be changed or removed in the future.
173 3
                        return sprintf('fragment %s', $node->name)
174 3
                            . $this->wrap('(', $this->join($node->variableDefinitions, ', '), ')')
175 3
                            . sprintf(' on %s ', $node->typeCondition)
176 3
                            . $this->wrap('', $this->join($node->directives, ' '), ' ')
177 3
                            . $node->selectionSet;
178 140
                    },
179
180
                    NodeKind::INT => static function (IntValueNode $node) {
181 21
                        return $node->value;
182 140
                    },
183
184
                    NodeKind::FLOAT => static function (FloatValueNode $node) {
185 8
                        return $node->value;
186 140
                    },
187
188
                    NodeKind::STRING => function (StringValueNode $node, $key) {
189 29
                        if ($node->block) {
190 8
                            return $this->printBlockString($node->value, $key === 'description');
191
                        }
192
193 25
                        return json_encode($node->value);
194 140
                    },
195
196
                    NodeKind::BOOLEAN => static function (BooleanValueNode $node) {
197 12
                        return $node->value ? 'true' : 'false';
198 140
                    },
199
200
                    NodeKind::NULL => static function (NullValueNode $node) {
0 ignored issues
show
Unused Code introduced by
The parameter $node is not used and could be removed. ( Ignorable by Annotation )

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

200
                    NodeKind::NULL => static function (/** @scrutinizer ignore-unused */ NullValueNode $node) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
201 8
                        return 'null';
202 140
                    },
203
204
                    NodeKind::ENUM => static function (EnumValueNode $node) {
205 18
                        return $node->value;
206 140
                    },
207
208
                    NodeKind::LST => function (ListValueNode $node) {
209 5
                        return '[' . $this->join($node->values, ', ') . ']';
210 140
                    },
211
212
                    NodeKind::OBJECT => function (ObjectValueNode $node) {
213 4
                        return '{' . $this->join($node->fields, ', ') . '}';
214 140
                    },
215
216
                    NodeKind::OBJECT_FIELD => static function (ObjectFieldNode $node) {
217 4
                        return $node->name . ': ' . $node->value;
0 ignored issues
show
Bug introduced by
Are you sure $node->value of type GraphQL\Language\AST\ValueNode can be used in concatenation? Consider adding a __toString()-method. ( Ignorable by Annotation )

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

217
                        return $node->name . ': ' . /** @scrutinizer ignore-type */ $node->value;
Loading history...
218 140
                    },
219
220
                    NodeKind::DIRECTIVE => function (DirectiveNode $node) {
221 7
                        return '@' . $node->name . $this->wrap('(', $this->join($node->arguments, ', '), ')');
222 140
                    },
223
224
                    NodeKind::NAMED_TYPE => static function (NamedTypeNode $node) {
225 68
                        return $node->name;
226 140
                    },
227
228
                    NodeKind::LIST_TYPE => static function (ListTypeNode $node) {
229 58
                        return '[' . $node->type . ']';
0 ignored issues
show
Bug introduced by
Are you sure $node->type of type GraphQL\Language\AST\TypeNode can be used in concatenation? Consider adding a __toString()-method. ( Ignorable by Annotation )

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

229
                        return '[' . /** @scrutinizer ignore-type */ $node->type . ']';
Loading history...
230 140
                    },
231
232
                    NodeKind::NON_NULL_TYPE => static function (NonNullTypeNode $node) {
233 60
                        return $node->type . '!';
234 140
                    },
235
236
                    NodeKind::SCHEMA_DEFINITION => function (SchemaDefinitionNode $def) {
237 3
                        return $this->join(
238
                            [
239 3
                                'schema',
240 3
                                $this->join($def->directives, ' '),
241 3
                                $this->block($def->operationTypes),
242
                            ],
243 3
                            ' '
244
                        );
245 140
                    },
246
247
                    NodeKind::OPERATION_TYPE_DEFINITION => static function (OperationTypeDefinitionNode $def) {
248 4
                        return $def->operation . ': ' . $def->type;
249 140
                    },
250
251
                    NodeKind::SCALAR_TYPE_DEFINITION => $this->addDescription(function (ScalarTypeDefinitionNode $def) {
252 59
                        return $this->join(['scalar', $def->name, $this->join($def->directives, ' ')], ' ');
253 140
                    }),
254
255
                    NodeKind::OBJECT_TYPE_DEFINITION => $this->addDescription(function (ObjectTypeDefinitionNode $def) {
256 58
                        return $this->join(
257
                            [
258 58
                                'type',
259 58
                                $def->name,
260 58
                                $this->wrap('implements ', $this->join($def->interfaces, ' & ')),
261 58
                                $this->join($def->directives, ' '),
262 58
                                $this->block($def->fields),
263
                            ],
264 58
                            ' '
265
                        );
266 140
                    }),
267
268
                    NodeKind::FIELD_DEFINITION => $this->addDescription(function (FieldDefinitionNode $def) {
269
                        $noIndent = Utils::every($def->arguments, static function (string $arg) {
270 58
                            return strpos($arg, "\n") === false;
271 58
                        });
272
273 58
                        return $def->name
274 58
                            . ($noIndent
275 58
                                ? $this->wrap('(', $this->join($def->arguments, ', '), ')')
276 58
                                : $this->wrap("(\n", $this->indent($this->join($def->arguments, "\n")), "\n)"))
277 58
                            . ': ' . $def->type
0 ignored issues
show
Bug introduced by
Are you sure $def->type of type GraphQL\Language\AST\TypeNode can be used in concatenation? Consider adding a __toString()-method. ( Ignorable by Annotation )

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

277
                            . ': ' . /** @scrutinizer ignore-type */ $def->type
Loading history...
278 58
                            . $this->wrap(' ', $this->join($def->directives, ' '));
279 140
                    }),
280
281
                    NodeKind::INPUT_VALUE_DEFINITION => $this->addDescription(function (InputValueDefinitionNode $def) {
282 58
                        return $this->join(
283
                            [
284 58
                                $def->name . ': ' . $def->type,
0 ignored issues
show
Bug introduced by
Are you sure $def->type of type GraphQL\Language\AST\TypeNode can be used in concatenation? Consider adding a __toString()-method. ( Ignorable by Annotation )

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

284
                                $def->name . ': ' . /** @scrutinizer ignore-type */ $def->type,
Loading history...
285 58
                                $this->wrap('= ', $def->defaultValue),
286 58
                                $this->join($def->directives, ' '),
287
                            ],
288 58
                            ' '
289
                        );
290 140
                    }),
291
292 140
                    NodeKind::INTERFACE_TYPE_DEFINITION => $this->addDescription(
293
                        function (InterfaceTypeDefinitionNode $def) {
294 58
                            return $this->join(
295
                                [
296 58
                                    'interface',
297 58
                                    $def->name,
298 58
                                    $this->join($def->directives, ' '),
299 58
                                    $this->block($def->fields),
300
                                ],
301 58
                                ' '
302
                            );
303 140
                        }
304
                    ),
305
306
                    NodeKind::UNION_TYPE_DEFINITION => $this->addDescription(function (UnionTypeDefinitionNode $def) {
307 58
                        return $this->join(
308
                            [
309 58
                                'union',
310 58
                                $def->name,
311 58
                                $this->join($def->directives, ' '),
312 58
                                $def->types
313 58
                                    ? '= ' . $this->join($def->types, ' | ')
314 58
                                    : '',
315
                            ],
316 58
                            ' '
317
                        );
318 140
                    }),
319
320
                    NodeKind::ENUM_TYPE_DEFINITION => $this->addDescription(function (EnumTypeDefinitionNode $def) {
321 58
                        return $this->join(
322
                            [
323 58
                                'enum',
324 58
                                $def->name,
325 58
                                $this->join($def->directives, ' '),
326 58
                                $this->block($def->values),
327
                            ],
328 58
                            ' '
329
                        );
330 140
                    }),
331
332
                    NodeKind::ENUM_VALUE_DEFINITION => $this->addDescription(function (EnumValueDefinitionNode $def) {
333 58
                        return $this->join([$def->name, $this->join($def->directives, ' ')], ' ');
334 140
                    }),
335
336
                    NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $this->addDescription(function (
337
                        InputObjectTypeDefinitionNode $def
338
                    ) {
339 58
                        return $this->join(
340
                            [
341 58
                                'input',
342 58
                                $def->name,
343 58
                                $this->join($def->directives, ' '),
344 58
                                $this->block($def->fields),
345
                            ],
346 58
                            ' '
347
                        );
348 140
                    }),
349
350
                    NodeKind::SCHEMA_EXTENSION => function (SchemaTypeExtensionNode $def) {
351 1
                        return $this->join(
352
                            [
353 1
                                'extend schema',
354 1
                                $this->join($def->directives, ' '),
355 1
                                $this->block($def->operationTypes),
356
                            ],
357 1
                            ' '
358
                        );
359 140
                    },
360
361
                    NodeKind::SCALAR_TYPE_EXTENSION => function (ScalarTypeExtensionNode $def) {
362 3
                        return $this->join(
363
                            [
364 3
                                'extend scalar',
365 3
                                $def->name,
366 3
                                $this->join($def->directives, ' '),
367
                            ],
368 3
                            ' '
369
                        );
370 140
                    },
371
372
                    NodeKind::OBJECT_TYPE_EXTENSION => function (ObjectTypeExtensionNode $def) {
373 2
                        return $this->join(
374
                            [
375 2
                                'extend type',
376 2
                                $def->name,
377 2
                                $this->wrap('implements ', $this->join($def->interfaces, ' & ')),
378 2
                                $this->join($def->directives, ' '),
379 2
                                $this->block($def->fields),
380
                            ],
381 2
                            ' '
382
                        );
383 140
                    },
384
385
                    NodeKind::INTERFACE_TYPE_EXTENSION => function (InterfaceTypeExtensionNode $def) {
386 2
                        return $this->join(
387
                            [
388 2
                                'extend interface',
389 2
                                $def->name,
390 2
                                $this->join($def->directives, ' '),
391 2
                                $this->block($def->fields),
392
                            ],
393 2
                            ' '
394
                        );
395 140
                    },
396
397
                    NodeKind::UNION_TYPE_EXTENSION => function (UnionTypeExtensionNode $def) {
398 2
                        return $this->join(
399
                            [
400 2
                                'extend union',
401 2
                                $def->name,
402 2
                                $this->join($def->directives, ' '),
403 2
                                $def->types
404 2
                                    ? '= ' . $this->join($def->types, ' | ')
405 2
                                    : '',
406
                            ],
407 2
                            ' '
408
                        );
409 140
                    },
410
411
                    NodeKind::ENUM_TYPE_EXTENSION => function (EnumTypeExtensionNode $def) {
412 2
                        return $this->join(
413
                            [
414 2
                                'extend enum',
415 2
                                $def->name,
416 2
                                $this->join($def->directives, ' '),
417 2
                                $this->block($def->values),
418
                            ],
419 2
                            ' '
420
                        );
421 140
                    },
422
423
                    NodeKind::INPUT_OBJECT_TYPE_EXTENSION => function (InputObjectTypeExtensionNode $def) {
424 2
                        return $this->join(
425
                            [
426 2
                                'extend input',
427 2
                                $def->name,
428 2
                                $this->join($def->directives, ' '),
429 2
                                $this->block($def->fields),
430
                            ],
431 2
                            ' '
432
                        );
433 140
                    },
434
435
                    NodeKind::DIRECTIVE_DEFINITION => $this->addDescription(function (DirectiveDefinitionNode $def) {
436
                        $noIndent = Utils::every($def->arguments, static function (string $arg) {
437 58
                            return strpos($arg, "\n") === false;
438 58
                        });
439
440
                        return 'directive @'
441 58
                            . $def->name
442 58
                            . ($noIndent
443 58
                                ? $this->wrap('(', $this->join($def->arguments, ', '), ')')
444 58
                                : $this->wrap("(\n", $this->indent($this->join($def->arguments, "\n")), "\n"))
445 58
                            . ' on ' . $this->join($def->locations, ' | ');
446 140
                    }),
447
                ],
448
            ]
449
        );
450
    }
451
452 140
    public function addDescription(callable $cb)
453
    {
454
        return function ($node) use ($cb) {
455 59
            return $this->join([$node->description, $cb($node)], "\n");
456 140
        };
457
    }
458
459
    /**
460
     * If maybeString is not null or empty, then wrap with start and end, otherwise
461
     * print an empty string.
462
     */
463 68
    public function wrap($start, $maybeString, $end = '')
464
    {
465 68
        return $maybeString ? ($start . $maybeString . $end) : '';
466
    }
467
468
    /**
469
     * Given array, print each item on its own line, wrapped in an
470
     * indented "{ }" block.
471
     */
472 67
    public function block($array)
473
    {
474 67
        return $array && $this->length($array)
475 67
            ? "{\n" . $this->indent($this->join($array, "\n")) . "\n}"
476 67
            : '';
477
    }
478
479 67
    public function indent($maybeString)
480
    {
481 67
        return $maybeString ? '  ' . str_replace("\n", "\n  ", $maybeString) : '';
482
    }
483
484
    public function manyList($start, $list, $separator, $end)
485
    {
486
        return $this->length($list) === 0 ? null : ($start . $this->join($list, $separator) . $end);
487
    }
488
489 67
    public function length($maybeArray)
490
    {
491 67
        return $maybeArray ? count($maybeArray) : 0;
492
    }
493
494 70
    public function join($maybeArray, $separator = '')
495
    {
496 70
        return $maybeArray
497 70
            ? implode(
498 70
                $separator,
499 70
                Utils::filter(
500 70
                    $maybeArray,
501
                    static function ($x) {
502 70
                        return (bool) $x;
503 70
                    }
504
                )
505
            )
506 70
            : '';
507
    }
508
509
    /**
510
     * Print a block string in the indented block form by adding a leading and
511
     * trailing blank line. However, if a block string starts with whitespace and is
512
     * a single-line, adding a leading blank line would strip that whitespace.
513
     */
514 8
    private function printBlockString($value, $isDescription)
515
    {
516 8
        $escaped = str_replace('"""', '\\"""', $value);
517
518 8
        return ($value[0] === ' ' || $value[0] === "\t") && strpos($value, "\n") === false
519 3
            ? ('"""' . preg_replace('/"$/', "\"\n", $escaped) . '"""')
520 8
            : ('"""' . "\n" . ($isDescription ? $escaped : $this->indent($escaped)) . "\n" . '"""');
521
    }
522
}
523