Issues (162)

src/Language/Printer.php (3 issues)

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 144
    public static function doPrint($ast)
85
    {
86 144
        static $instance;
87 144
        $instance = $instance ?: new static();
88
89 144
        return $instance->printAST($ast);
90
    }
91
92 1
    protected function __construct()
93
    {
94 1
    }
95
96 144
    public function printAST($ast)
97
    {
98 144
        return Visitor::visit(
99 144
            $ast,
100
            [
101
                'leave' => [
102
                    NodeKind::NAME => static function (NameNode $node) {
103 77
                        return '' . $node->value;
104 144
                    },
105
106
                    NodeKind::VARIABLE => static function (VariableNode $node) {
107 6
                        return '$' . $node->name;
108 144
                    },
109
110
                    NodeKind::DOCUMENT => function (DocumentNode $node) {
111 31
                        return $this->join($node->definitions, "\n\n") . "\n";
112 144
                    },
113
114
                    NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) {
115 9
                        $op           = $node->operation;
116 9
                        $name         = $node->name;
117 9
                        $varDefs      = $this->wrap('(', $this->join($node->variableDefinitions, ', '), ')');
118 9
                        $directives   = $this->join($node->directives, ' ');
119 9
                        $selectionSet = $node->selectionSet;
120
121
                        // Anonymous queries with no directives or variable definitions can use
122
                        // the query short form.
123 9
                        return ! $name && ! $directives && ! $varDefs && $op === 'query'
0 ignored issues
show
$name is of type GraphQL\Language\AST\NameNode, thus it always evaluated to true.
Loading history...
124 8
                            ? $selectionSet
125 9
                            : $this->join([$op, $this->join([$name, $varDefs]), $directives, $selectionSet], ' ');
126 144
                    },
127
128
                    NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) {
129 6
                        return $node->variable
130 6
                            . ': '
131 6
                            . $node->type
132 6
                            . $this->wrap(' = ', $node->defaultValue)
133 6
                            . $this->wrap(' ', $this->join($node->directives, ' '));
134 144
                    },
135
136
                    NodeKind::SELECTION_SET => function (SelectionSetNode $node) {
137 11
                        return $this->block($node->selections);
138 144
                    },
139
140
                    NodeKind::FIELD => function (FieldNode $node) {
141 12
                        return $this->join(
142
                            [
143 12
                                $this->wrap('', $node->alias, ': ') . $node->name . $this->wrap(
144 12
                                    '(',
145 12
                                    $this->join($node->arguments, ', '),
146 12
                                    ')'
147
                                ),
148 12
                                $this->join($node->directives, ' '),
149 12
                                $node->selectionSet,
150
                            ],
151 12
                            ' '
152
                        );
153 144
                    },
154
155
                    NodeKind::ARGUMENT => static function (ArgumentNode $node) {
156 10
                        return $node->name . ': ' . $node->value;
157 144
                    },
158
159
                    NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) {
160 2
                        return '...' . $node->name . $this->wrap(' ', $this->join($node->directives, ' '));
161 144
                    },
162
163
                    NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) {
164 2
                        return $this->join(
165
                            [
166 2
                                '...',
167 2
                                $this->wrap('on ', $node->typeCondition),
168 2
                                $this->join($node->directives, ' '),
169 2
                                $node->selectionSet,
170
                            ],
171 2
                            ' '
172
                        );
173 144
                    },
174
175
                    NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) {
176
                        // Note: fragment variable definitions are experimental and may be changed or removed in the future.
177 4
                        return sprintf('fragment %s', $node->name)
178 4
                            . $this->wrap('(', $this->join($node->variableDefinitions, ', '), ')')
179 4
                            . sprintf(' on %s ', $node->typeCondition)
180 4
                            . $this->wrap('', $this->join($node->directives, ' '), ' ')
181 4
                            . $node->selectionSet;
182 144
                    },
183
184
                    NodeKind::INT => static function (IntValueNode $node) {
185 22
                        return $node->value;
186 144
                    },
187
188
                    NodeKind::FLOAT => static function (FloatValueNode $node) {
189 8
                        return $node->value;
190 144
                    },
191
192
                    NodeKind::STRING => function (StringValueNode $node, $key) {
193 30
                        if ($node->block) {
194 8
                            return $this->printBlockString($node->value, $key === 'description');
195
                        }
196
197 26
                        return json_encode($node->value);
198 144
                    },
199
200
                    NodeKind::BOOLEAN => static function (BooleanValueNode $node) {
201 14
                        return $node->value ? 'true' : 'false';
202 144
                    },
203
204
                    NodeKind::NULL => static function (NullValueNode $node) {
0 ignored issues
show
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

204
                    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...
205 9
                        return 'null';
206 144
                    },
207
208
                    NodeKind::ENUM => static function (EnumValueNode $node) {
209 18
                        return $node->value;
210 144
                    },
211
212
                    NodeKind::LST => function (ListValueNode $node) {
213 5
                        return '[' . $this->join($node->values, ', ') . ']';
214 144
                    },
215
216
                    NodeKind::OBJECT => function (ObjectValueNode $node) {
217 5
                        return '{' . $this->join($node->fields, ', ') . '}';
218 144
                    },
219
220
                    NodeKind::OBJECT_FIELD => static function (ObjectFieldNode $node) {
221 5
                        return $node->name . ': ' . $node->value;
222 144
                    },
223
224
                    NodeKind::DIRECTIVE => function (DirectiveNode $node) {
225 9
                        return '@' . $node->name . $this->wrap('(', $this->join($node->arguments, ', '), ')');
226 144
                    },
227
228
                    NodeKind::NAMED_TYPE => static function (NamedTypeNode $node) {
229 70
                        return $node->name;
230 144
                    },
231
232
                    NodeKind::LIST_TYPE => static function (ListTypeNode $node) {
233 58
                        return '[' . $node->type . ']';
234 144
                    },
235
236
                    NodeKind::NON_NULL_TYPE => static function (NonNullTypeNode $node) {
237 60
                        return $node->type . '!';
238 144
                    },
239
240
                    NodeKind::SCHEMA_DEFINITION => function (SchemaDefinitionNode $def) {
241 3
                        return $this->join(
242
                            [
243 3
                                'schema',
244 3
                                $this->join($def->directives, ' '),
245 3
                                $this->block($def->operationTypes),
246
                            ],
247 3
                            ' '
248
                        );
249 144
                    },
250
251
                    NodeKind::OPERATION_TYPE_DEFINITION => static function (OperationTypeDefinitionNode $def) {
252 4
                        return $def->operation . ': ' . $def->type;
253 144
                    },
254
255
                    NodeKind::SCALAR_TYPE_DEFINITION => $this->addDescription(function (ScalarTypeDefinitionNode $def) {
256 59
                        return $this->join(['scalar', $def->name, $this->join($def->directives, ' ')], ' ');
257 144
                    }),
258
259
                    NodeKind::OBJECT_TYPE_DEFINITION => $this->addDescription(function (ObjectTypeDefinitionNode $def) {
260 58
                        return $this->join(
261
                            [
262 58
                                'type',
263 58
                                $def->name,
264 58
                                $this->wrap('implements ', $this->join($def->interfaces, ' & ')),
265 58
                                $this->join($def->directives, ' '),
266 58
                                $this->block($def->fields),
267
                            ],
268 58
                            ' '
269
                        );
270 144
                    }),
271
272
                    NodeKind::FIELD_DEFINITION => $this->addDescription(function (FieldDefinitionNode $def) {
273
                        $noIndent = Utils::every($def->arguments, static function (string $arg) {
0 ignored issues
show
$def->arguments of type GraphQL\Language\AST\NodeList is incompatible with the type array<mixed,mixed> expected by parameter $traversable of GraphQL\Utils\Utils::every(). ( Ignorable by Annotation )

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

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