1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\PhpDocParser\Printer;
4:
5: use LogicException;
6: use PHPStan\PhpDocParser\Ast\Attribute;
7: use PHPStan\PhpDocParser\Ast\Comment;
8: use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode;
9: use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode;
10: use PHPStan\PhpDocParser\Ast\Node;
11: use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagMethodValueNode;
12: use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagPropertyValueNode;
13: use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagValueNode;
14: use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineAnnotation;
15: use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArgument;
16: use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArray;
17: use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArrayItem;
18: use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineTagValueNode;
19: use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode;
20: use PHPStan\PhpDocParser\Ast\PhpDoc\ImplementsTagValueNode;
21: use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode;
22: use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode;
23: use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode;
24: use PHPStan\PhpDocParser\Ast\PhpDoc\ParamClosureThisTagValueNode;
25: use PHPStan\PhpDocParser\Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode;
26: use PHPStan\PhpDocParser\Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode;
27: use PHPStan\PhpDocParser\Ast\PhpDoc\ParamOutTagValueNode;
28: use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
29: use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode;
30: use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
31: use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
32: use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
33: use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
34: use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
35: use PHPStan\PhpDocParser\Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode;
36: use PHPStan\PhpDocParser\Ast\PhpDoc\PureUnlessParameterIsPassedTagValueNode;
37: use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode;
38: use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode;
39: use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
40: use PHPStan\PhpDocParser\Ast\PhpDoc\SealedTagValueNode;
41: use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode;
42: use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
43: use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode;
44: use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasImportTagValueNode;
45: use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode;
46: use PHPStan\PhpDocParser\Ast\PhpDoc\UsesTagValueNode;
47: use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
48: use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode;
49: use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
50: use PHPStan\PhpDocParser\Ast\Type\ArrayShapeUnsealedTypeNode;
51: use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
52: use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
53: use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
54: use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode;
55: use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode;
56: use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
57: use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
58: use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
59: use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
60: use PHPStan\PhpDocParser\Ast\Type\InvalidTypeNode;
61: use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
62: use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode;
63: use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
64: use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
65: use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode;
66: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
67: use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
68: use PHPStan\PhpDocParser\Lexer\Lexer;
69: use PHPStan\PhpDocParser\Parser\TokenIterator;
70: use function array_keys;
71: use function array_map;
72: use function assert;
73: use function count;
74: use function get_class;
75: use function get_object_vars;
76: use function implode;
77: use function in_array;
78: use function is_array;
79: use function preg_match_all;
80: use function sprintf;
81: use function str_replace;
82: use function strlen;
83: use function strpos;
84: use function trim;
85: use const PREG_SET_ORDER;
86:
87: /**
88: * Inspired by https://github.com/nikic/PHP-Parser/tree/36a6dcd04e7b0285e8f0868f44bd4927802f7df1
89: *
90: * Copyright (c) 2011, Nikita Popov
91: * All rights reserved.
92: */
93: final class Printer
94: {
95:
96: /** @var Differ<Node> */
97: private Differ $differ;
98:
99: /**
100: * Map From "{$class}->{$subNode}" to string that should be inserted
101: * between elements of this list subnode
102: *
103: * @var array<string, string>
104: */
105: private array $listInsertionMap = [
106: PhpDocNode::class . '->children' => "\n * ",
107: UnionTypeNode::class . '->types' => '|',
108: IntersectionTypeNode::class . '->types' => '&',
109: ArrayShapeNode::class . '->items' => ', ',
110: ObjectShapeNode::class . '->items' => ', ',
111: CallableTypeNode::class . '->parameters' => ', ',
112: CallableTypeNode::class . '->templateTypes' => ', ',
113: GenericTypeNode::class . '->genericTypes' => ', ',
114: ConstExprArrayNode::class . '->items' => ', ',
115: MethodTagValueNode::class . '->parameters' => ', ',
116: DoctrineArray::class . '->items' => ', ',
117: DoctrineAnnotation::class . '->arguments' => ', ',
118: ];
119:
120: /**
121: * [$find, $extraLeft, $extraRight]
122: *
123: * @var array<string, array{string|null, string, string}>
124: */
125: private array $emptyListInsertionMap = [
126: CallableTypeNode::class . '->parameters' => ['(', '', ''],
127: ArrayShapeNode::class . '->items' => ['{', '', ''],
128: ObjectShapeNode::class . '->items' => ['{', '', ''],
129: DoctrineArray::class . '->items' => ['{', '', ''],
130: DoctrineAnnotation::class . '->arguments' => ['(', '', ''],
131: ];
132:
133: /** @var array<string, list<class-string<TypeNode>>> */
134: private array $parenthesesMap = [
135: CallableTypeNode::class . '->returnType' => [
136: CallableTypeNode::class,
137: UnionTypeNode::class,
138: IntersectionTypeNode::class,
139: ],
140: ArrayTypeNode::class . '->type' => [
141: CallableTypeNode::class,
142: UnionTypeNode::class,
143: IntersectionTypeNode::class,
144: ConstTypeNode::class,
145: NullableTypeNode::class,
146: ],
147: OffsetAccessTypeNode::class . '->type' => [
148: CallableTypeNode::class,
149: UnionTypeNode::class,
150: IntersectionTypeNode::class,
151: NullableTypeNode::class,
152: ],
153: NullableTypeNode::class . '->type' => [
154: UnionTypeNode::class,
155: IntersectionTypeNode::class,
156: NullableTypeNode::class,
157: ],
158: ConditionalTypeNode::class . '->subjectType' => [
159: UnionTypeNode::class,
160: IntersectionTypeNode::class,
161: NullableTypeNode::class,
162: ],
163: ];
164:
165: /** @var array<string, list<class-string<TypeNode>>> */
166: private array $parenthesesListMap = [
167: IntersectionTypeNode::class . '->types' => [
168: IntersectionTypeNode::class,
169: UnionTypeNode::class,
170: NullableTypeNode::class,
171: ],
172: UnionTypeNode::class . '->types' => [
173: IntersectionTypeNode::class,
174: UnionTypeNode::class,
175: NullableTypeNode::class,
176: ],
177: ];
178:
179: public function printFormatPreserving(PhpDocNode $node, PhpDocNode $originalNode, TokenIterator $originalTokens): string
180: {
181: $this->differ = new Differ(static function ($a, $b) {
182: if ($a instanceof Node && $b instanceof Node) {
183: return $a === $b->getAttribute(Attribute::ORIGINAL_NODE);
184: }
185:
186: return false;
187: });
188:
189: $tokenIndex = 0;
190: $result = $this->printArrayFormatPreserving(
191: $node->children,
192: $originalNode->children,
193: $originalTokens,
194: $tokenIndex,
195: PhpDocNode::class,
196: 'children',
197: );
198: if ($result !== null) {
199: return $result . $originalTokens->getContentBetween($tokenIndex, $originalTokens->getTokenCount());
200: }
201:
202: return $this->print($node);
203: }
204:
205: public function print(Node $node): string
206: {
207: if ($node instanceof PhpDocNode) {
208: return "/**\n *" . implode("\n *", array_map(
209: function (PhpDocChildNode $child): string {
210: $s = $this->print($child);
211: return $s === '' ? '' : ' ' . $s;
212: },
213: $node->children,
214: )) . "\n */";
215: }
216: if ($node instanceof PhpDocTextNode) {
217: return $node->text;
218: }
219: if ($node instanceof PhpDocTagNode) {
220: if ($node->value instanceof DoctrineTagValueNode) {
221: return $this->print($node->value);
222: }
223:
224: return trim(sprintf('%s %s', $node->name, $this->print($node->value)));
225: }
226: if ($node instanceof PhpDocTagValueNode) {
227: return $this->printTagValue($node);
228: }
229: if ($node instanceof TypeNode) {
230: return $this->printType($node);
231: }
232: if ($node instanceof ConstExprNode) {
233: return $this->printConstExpr($node);
234: }
235: if ($node instanceof MethodTagValueParameterNode) {
236: $type = $node->type !== null ? $this->print($node->type) . ' ' : '';
237: $isReference = $node->isReference ? '&' : '';
238: $isVariadic = $node->isVariadic ? '...' : '';
239: $default = $node->defaultValue !== null ? ' = ' . $this->print($node->defaultValue) : '';
240: return "{$type}{$isReference}{$isVariadic}{$node->parameterName}{$default}";
241: }
242: if ($node instanceof CallableTypeParameterNode) {
243: $type = $this->print($node->type) . ' ';
244: $isReference = $node->isReference ? '&' : '';
245: $isVariadic = $node->isVariadic ? '...' : '';
246: $isOptional = $node->isOptional ? '=' : '';
247: return trim("{$type}{$isReference}{$isVariadic}{$node->parameterName}") . $isOptional;
248: }
249: if ($node instanceof ArrayShapeUnsealedTypeNode) {
250: if ($node->keyType !== null) {
251: return sprintf('<%s, %s>', $this->printType($node->keyType), $this->printType($node->valueType));
252: }
253: return sprintf('<%s>', $this->printType($node->valueType));
254: }
255: if ($node instanceof DoctrineAnnotation) {
256: return (string) $node;
257: }
258: if ($node instanceof DoctrineArgument) {
259: return (string) $node;
260: }
261: if ($node instanceof DoctrineArray) {
262: return (string) $node;
263: }
264: if ($node instanceof DoctrineArrayItem) {
265: return (string) $node;
266: }
267: if ($node instanceof ArrayShapeItemNode) {
268: if ($node->keyName !== null) {
269: return sprintf(
270: '%s%s: %s',
271: $this->print($node->keyName),
272: $node->optional ? '?' : '',
273: $this->printType($node->valueType),
274: );
275: }
276:
277: return $this->printType($node->valueType);
278: }
279: if ($node instanceof ObjectShapeItemNode) {
280: if ($node->keyName !== null) {
281: return sprintf(
282: '%s%s: %s',
283: $this->print($node->keyName),
284: $node->optional ? '?' : '',
285: $this->printType($node->valueType),
286: );
287: }
288:
289: return $this->printType($node->valueType);
290: }
291:
292: throw new LogicException(sprintf('Unknown node type %s', get_class($node)));
293: }
294:
295: private function printTagValue(PhpDocTagValueNode $node): string
296: {
297: // only nodes that contain another node are handled here
298: // the rest falls back on (string) $node
299:
300: if ($node instanceof AssertTagMethodValueNode) {
301: $isNegated = $node->isNegated ? '!' : '';
302: $isEquality = $node->isEquality ? '=' : '';
303: $type = $this->printType($node->type);
304: return trim("{$isNegated}{$isEquality}{$type} {$node->parameter}->{$node->method}() {$node->description}");
305: }
306: if ($node instanceof AssertTagPropertyValueNode) {
307: $isNegated = $node->isNegated ? '!' : '';
308: $isEquality = $node->isEquality ? '=' : '';
309: $type = $this->printType($node->type);
310: return trim("{$isNegated}{$isEquality}{$type} {$node->parameter}->{$node->property} {$node->description}");
311: }
312: if ($node instanceof AssertTagValueNode) {
313: $isNegated = $node->isNegated ? '!' : '';
314: $isEquality = $node->isEquality ? '=' : '';
315: $type = $this->printType($node->type);
316: return trim("{$isNegated}{$isEquality}{$type} {$node->parameter} {$node->description}");
317: }
318: if ($node instanceof ExtendsTagValueNode || $node instanceof ImplementsTagValueNode) {
319: $type = $this->printType($node->type);
320: return trim("{$type} {$node->description}");
321: }
322: if ($node instanceof MethodTagValueNode) {
323: $static = $node->isStatic ? 'static ' : '';
324: $returnType = $node->returnType !== null ? $this->printType($node->returnType) . ' ' : '';
325: $parameters = implode(', ', array_map(fn (MethodTagValueParameterNode $parameter): string => $this->print($parameter), $node->parameters));
326: $description = $node->description !== '' ? " {$node->description}" : '';
327: $templateTypes = count($node->templateTypes) > 0 ? '<' . implode(', ', array_map(fn (TemplateTagValueNode $templateTag): string => $this->print($templateTag), $node->templateTypes)) . '>' : '';
328: return "{$static}{$returnType}{$node->methodName}{$templateTypes}({$parameters}){$description}";
329: }
330: if ($node instanceof MixinTagValueNode) {
331: $type = $this->printType($node->type);
332: return trim("{$type} {$node->description}");
333: }
334: if ($node instanceof RequireExtendsTagValueNode) {
335: $type = $this->printType($node->type);
336: return trim("{$type} {$node->description}");
337: }
338: if ($node instanceof RequireImplementsTagValueNode) {
339: $type = $this->printType($node->type);
340: return trim("{$type} {$node->description}");
341: }
342: if ($node instanceof SealedTagValueNode) {
343: $type = $this->printType($node->type);
344: return trim("{$type} {$node->description}");
345: }
346: if ($node instanceof ParamOutTagValueNode) {
347: $type = $this->printType($node->type);
348: return trim("{$type} {$node->parameterName} {$node->description}");
349: }
350: if ($node instanceof ParamTagValueNode) {
351: $reference = $node->isReference ? '&' : '';
352: $variadic = $node->isVariadic ? '...' : '';
353: $type = $this->printType($node->type);
354: return trim("{$type} {$reference}{$variadic}{$node->parameterName} {$node->description}");
355: }
356: if ($node instanceof ParamImmediatelyInvokedCallableTagValueNode) {
357: return trim("{$node->parameterName} {$node->description}");
358: }
359: if ($node instanceof ParamLaterInvokedCallableTagValueNode) {
360: return trim("{$node->parameterName} {$node->description}");
361: }
362: if ($node instanceof ParamClosureThisTagValueNode) {
363: return trim("{$node->type} {$node->parameterName} {$node->description}");
364: }
365: if ($node instanceof PureUnlessCallableIsImpureTagValueNode) {
366: return trim("{$node->parameterName} {$node->description}");
367: }
368: if ($node instanceof PureUnlessParameterIsPassedTagValueNode) {
369: return trim("{$node->parameterName} {$node->description}");
370: }
371: if ($node instanceof PropertyTagValueNode) {
372: $type = $this->printType($node->type);
373: return trim("{$type} {$node->propertyName} {$node->description}");
374: }
375: if ($node instanceof ReturnTagValueNode) {
376: $type = $this->printType($node->type);
377: return trim("{$type} {$node->description}");
378: }
379: if ($node instanceof SelfOutTagValueNode) {
380: $type = $this->printType($node->type);
381: return trim($type . ' ' . $node->description);
382: }
383: if ($node instanceof TemplateTagValueNode) {
384: $upperBound = $node->bound !== null ? ' of ' . $this->printType($node->bound) : '';
385: $lowerBound = $node->lowerBound !== null ? ' super ' . $this->printType($node->lowerBound) : '';
386: $default = $node->default !== null ? ' = ' . $this->printType($node->default) : '';
387: return trim("{$node->name}{$upperBound}{$lowerBound}{$default} {$node->description}");
388: }
389: if ($node instanceof ThrowsTagValueNode) {
390: $type = $this->printType($node->type);
391: return trim("{$type} {$node->description}");
392: }
393: if ($node instanceof TypeAliasImportTagValueNode) {
394: return trim(
395: "{$node->importedAlias} from " . $this->printType($node->importedFrom)
396: . ($node->importedAs !== null ? " as {$node->importedAs}" : ''),
397: );
398: }
399: if ($node instanceof TypeAliasTagValueNode) {
400: $type = $this->printType($node->type);
401: return trim("{$node->alias} {$type}");
402: }
403: if ($node instanceof UsesTagValueNode) {
404: $type = $this->printType($node->type);
405: return trim("{$type} {$node->description}");
406: }
407: if ($node instanceof VarTagValueNode) {
408: $type = $this->printType($node->type);
409: return trim("{$type} " . trim("{$node->variableName} {$node->description}"));
410: }
411:
412: return (string) $node;
413: }
414:
415: private function printType(TypeNode $node): string
416: {
417: if ($node instanceof ArrayShapeNode) {
418: $items = array_map(fn (ArrayShapeItemNode $item): string => $this->print($item), $node->items);
419:
420: if (! $node->sealed) {
421: $items[] = '...' . ($node->unsealedType === null ? '' : $this->print($node->unsealedType));
422: }
423:
424: return $node->kind . '{' . implode(', ', $items) . '}';
425: }
426: if ($node instanceof ArrayTypeNode) {
427: return $this->printOffsetAccessType($node->type) . '[]';
428: }
429: if ($node instanceof CallableTypeNode) {
430: if ($node->returnType instanceof CallableTypeNode || $node->returnType instanceof UnionTypeNode || $node->returnType instanceof IntersectionTypeNode) {
431: $returnType = $this->wrapInParentheses($node->returnType);
432: } else {
433: $returnType = $this->printType($node->returnType);
434: }
435: $template = $node->templateTypes !== []
436: ? '<' . implode(', ', array_map(fn (TemplateTagValueNode $templateNode): string => $this->print($templateNode), $node->templateTypes)) . '>'
437: : '';
438: $parameters = implode(', ', array_map(fn (CallableTypeParameterNode $parameterNode): string => $this->print($parameterNode), $node->parameters));
439: return "{$node->identifier}{$template}({$parameters}): {$returnType}";
440: }
441: if ($node instanceof ConditionalTypeForParameterNode) {
442: return sprintf(
443: '(%s %s %s ? %s : %s)',
444: $node->parameterName,
445: $node->negated ? 'is not' : 'is',
446: $this->printType($node->targetType),
447: $this->printType($node->if),
448: $this->printType($node->else),
449: );
450: }
451: if ($node instanceof ConditionalTypeNode) {
452: return sprintf(
453: '(%s %s %s ? %s : %s)',
454: $this->printConditionalSubjectType($node->subjectType),
455: $node->negated ? 'is not' : 'is',
456: $this->printType($node->targetType),
457: $this->printType($node->if),
458: $this->printType($node->else),
459: );
460: }
461: if ($node instanceof ConstTypeNode) {
462: return $this->printConstExpr($node->constExpr);
463: }
464: if ($node instanceof GenericTypeNode) {
465: $genericTypes = [];
466:
467: foreach ($node->genericTypes as $index => $type) {
468: $variance = $node->variances[$index] ?? GenericTypeNode::VARIANCE_INVARIANT;
469: if ($variance === GenericTypeNode::VARIANCE_INVARIANT) {
470: $genericTypes[] = $this->printType($type);
471: } elseif ($variance === GenericTypeNode::VARIANCE_BIVARIANT) {
472: $genericTypes[] = '*';
473: } else {
474: $genericTypes[] = sprintf('%s %s', $variance, $this->print($type));
475: }
476: }
477:
478: return $node->type . '<' . implode(', ', $genericTypes) . '>';
479: }
480: if ($node instanceof IdentifierTypeNode) {
481: return $node->name;
482: }
483: if ($node instanceof IntersectionTypeNode || $node instanceof UnionTypeNode) {
484: $items = [];
485: foreach ($node->types as $type) {
486: if (
487: $type instanceof IntersectionTypeNode
488: || $type instanceof UnionTypeNode
489: || $type instanceof NullableTypeNode
490: ) {
491: $items[] = $this->wrapInParentheses($type);
492: continue;
493: }
494:
495: $items[] = $this->printType($type);
496: }
497:
498: return implode($node instanceof IntersectionTypeNode ? '&' : '|', $items);
499: }
500: if ($node instanceof InvalidTypeNode) {
501: return (string) $node;
502: }
503: if ($node instanceof NullableTypeNode) {
504: if (
505: $node->type instanceof IntersectionTypeNode
506: || $node->type instanceof UnionTypeNode
507: || $node->type instanceof NullableTypeNode
508: ) {
509: return '?(' . $this->printType($node->type) . ')';
510: }
511:
512: return '?' . $this->printType($node->type);
513: }
514: if ($node instanceof ObjectShapeNode) {
515: $items = array_map(fn (ObjectShapeItemNode $item): string => $this->print($item), $node->items);
516:
517: return 'object{' . implode(', ', $items) . '}';
518: }
519: if ($node instanceof OffsetAccessTypeNode) {
520: return $this->printOffsetAccessType($node->type) . '[' . $this->printType($node->offset) . ']';
521: }
522: if ($node instanceof ThisTypeNode) {
523: return (string) $node;
524: }
525:
526: throw new LogicException(sprintf('Unknown node type %s', get_class($node)));
527: }
528:
529: private function wrapInParentheses(TypeNode $node): string
530: {
531: return '(' . $this->printType($node) . ')';
532: }
533:
534: /**
535: * What a conditional type asks about, written so that it is read back as
536: * the very same type.
537: *
538: * "?Foo is Bar ? ... : ..." and "Foo|Bar is Baz ? ... : ..." both read as a
539: * type followed by something the type says nothing about, so a subject of
540: * either kind keeps its parentheses.
541: */
542: private function printConditionalSubjectType(TypeNode $type): string
543: {
544: if (
545: $type instanceof UnionTypeNode
546: || $type instanceof IntersectionTypeNode
547: || $type instanceof NullableTypeNode
548: ) {
549: return $this->wrapInParentheses($type);
550: }
551:
552: return $this->printType($type);
553: }
554:
555: private function printOffsetAccessType(TypeNode $type): string
556: {
557: if (
558: $type instanceof CallableTypeNode
559: || $type instanceof UnionTypeNode
560: || $type instanceof IntersectionTypeNode
561: || $type instanceof NullableTypeNode
562: ) {
563: return $this->wrapInParentheses($type);
564: }
565:
566: return $this->printType($type);
567: }
568:
569: private function printConstExpr(ConstExprNode $node): string
570: {
571: // this is fine - ConstExprNode classes do not contain nodes that need smart printer logic
572: return (string) $node;
573: }
574:
575: /**
576: * @param Node[] $nodes
577: * @param Node[] $originalNodes
578: */
579: private function printArrayFormatPreserving(array $nodes, array $originalNodes, TokenIterator $originalTokens, int &$tokenIndex, string $parentNodeClass, string $subNodeName): ?string
580: {
581: $diff = $this->differ->diffWithReplacements($originalNodes, $nodes);
582: $mapKey = $parentNodeClass . '->' . $subNodeName;
583: $insertStr = $this->listInsertionMap[$mapKey] ?? null;
584: $result = '';
585: $beforeFirstKeepOrReplace = true;
586: $delayedAdd = [];
587:
588: $insertNewline = false;
589: [$isMultiline, $beforeAsteriskIndent, $afterAsteriskIndent] = $this->isMultiline($tokenIndex, $originalNodes, $originalTokens);
590:
591: if ($insertStr === "\n * ") {
592: $insertStr = sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
593: }
594:
595: foreach ($diff as $i => $diffElem) {
596: $diffType = $diffElem->type;
597: $arrItem = $diffElem->new;
598: $origArrayItem = $diffElem->old;
599: if ($diffType === DiffElem::TYPE_KEEP || $diffType === DiffElem::TYPE_REPLACE) {
600: $beforeFirstKeepOrReplace = false;
601: if (!$arrItem instanceof Node || !$origArrayItem instanceof Node) {
602: return null;
603: }
604:
605: /** @var int $itemStartPos */
606: $itemStartPos = $origArrayItem->getAttribute(Attribute::START_INDEX);
607:
608: /** @var int $itemEndPos */
609: $itemEndPos = $origArrayItem->getAttribute(Attribute::END_INDEX);
610:
611: if ($itemStartPos < 0 || $itemEndPos < 0 || $itemStartPos < $tokenIndex) {
612: throw new LogicException();
613: }
614:
615: $comments = $arrItem->getAttribute(Attribute::COMMENTS) ?? [];
616: $origComments = $origArrayItem->getAttribute(Attribute::COMMENTS) ?? [];
617:
618: $commentStartPos = count($origComments) > 0 ? $origComments[0]->startIndex : $itemStartPos;
619: assert($commentStartPos >= 0);
620:
621: $result .= $originalTokens->getContentBetween($tokenIndex, $itemStartPos);
622:
623: if (count($delayedAdd) > 0) {
624: foreach ($delayedAdd as $delayedAddNode) {
625: $parenthesesNeeded = isset($this->parenthesesListMap[$mapKey])
626: && in_array(get_class($delayedAddNode), $this->parenthesesListMap[$mapKey], true);
627: if ($parenthesesNeeded) {
628: $result .= '(';
629: }
630:
631: if ($insertNewline) {
632: $delayedAddComments = $delayedAddNode->getAttribute(Attribute::COMMENTS) ?? [];
633: if (count($delayedAddComments) > 0) {
634: $result .= $this->printComments($delayedAddComments, $beforeAsteriskIndent, $afterAsteriskIndent);
635: $result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
636: }
637: }
638:
639: $result .= $this->printNodeFormatPreserving($delayedAddNode, $originalTokens);
640: if ($parenthesesNeeded) {
641: $result .= ')';
642: }
643:
644: if ($insertNewline) {
645: $result .= $insertStr . sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
646: } else {
647: $result .= $insertStr;
648: }
649: }
650:
651: $delayedAdd = [];
652: }
653:
654: $parenthesesNeeded = isset($this->parenthesesListMap[$mapKey])
655: && in_array(get_class($arrItem), $this->parenthesesListMap[$mapKey], true)
656: && !in_array(get_class($origArrayItem), $this->parenthesesListMap[$mapKey], true);
657: $addParentheses = $parenthesesNeeded && !$originalTokens->hasParentheses($itemStartPos, $itemEndPos);
658: if ($addParentheses) {
659: $result .= '(';
660: }
661:
662: if ($comments !== $origComments) {
663: if (count($comments) > 0) {
664: $result .= $this->printComments($comments, $beforeAsteriskIndent, $afterAsteriskIndent);
665: $result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
666: }
667: }
668:
669: $result .= $this->printNodeFormatPreserving($arrItem, $originalTokens);
670: if ($addParentheses) {
671: $result .= ')';
672: }
673: $tokenIndex = $itemEndPos + 1;
674:
675: } elseif ($diffType === DiffElem::TYPE_ADD) {
676: if ($insertStr === null) {
677: return null;
678: }
679: if (!$arrItem instanceof Node) {
680: return null;
681: }
682:
683: if ($insertStr === ', ' && $isMultiline || count($arrItem->getAttribute(Attribute::COMMENTS) ?? []) > 0) {
684: $insertStr = ',';
685: $insertNewline = true;
686: }
687:
688: if ($beforeFirstKeepOrReplace) {
689: // Will be inserted at the next "replace" or "keep" element
690: $delayedAdd[] = $arrItem;
691: continue;
692: }
693:
694: /** @var int $itemEndPos */
695: $itemEndPos = $tokenIndex - 1;
696: if ($insertNewline) {
697: $comments = $arrItem->getAttribute(Attribute::COMMENTS) ?? [];
698: $result .= $insertStr;
699: if (count($comments) > 0) {
700: $result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
701: $result .= $this->printComments($comments, $beforeAsteriskIndent, $afterAsteriskIndent);
702: }
703: $result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
704: } else {
705: $result .= $insertStr;
706: }
707:
708: $parenthesesNeeded = isset($this->parenthesesListMap[$mapKey])
709: && in_array(get_class($arrItem), $this->parenthesesListMap[$mapKey], true);
710: if ($parenthesesNeeded) {
711: $result .= '(';
712: }
713:
714: $result .= $this->printNodeFormatPreserving($arrItem, $originalTokens);
715: if ($parenthesesNeeded) {
716: $result .= ')';
717: }
718:
719: $tokenIndex = $itemEndPos + 1;
720:
721: } elseif ($diffType === DiffElem::TYPE_REMOVE) {
722: if (!$origArrayItem instanceof Node) {
723: return null;
724: }
725:
726: /** @var int $itemStartPos */
727: $itemStartPos = $origArrayItem->getAttribute(Attribute::START_INDEX);
728:
729: /** @var int $itemEndPos */
730: $itemEndPos = $origArrayItem->getAttribute(Attribute::END_INDEX);
731: if ($itemStartPos < 0 || $itemEndPos < 0) {
732: throw new LogicException();
733: }
734:
735: if ($i === 0) {
736: // If we're removing from the start, keep the tokens before the node and drop those after it,
737: // instead of the other way around.
738: $originalTokensArray = $originalTokens->getTokens();
739: for ($j = $tokenIndex; $j < $itemStartPos; $j++) {
740: if ($originalTokensArray[$j][Lexer::TYPE_OFFSET] === Lexer::TOKEN_PHPDOC_EOL) {
741: break;
742: }
743: $result .= $originalTokensArray[$j][Lexer::VALUE_OFFSET];
744: }
745: }
746:
747: $tokenIndex = $itemEndPos + 1;
748: }
749: }
750:
751: if (count($delayedAdd) > 0) {
752: if (!isset($this->emptyListInsertionMap[$mapKey])) {
753: return null;
754: }
755:
756: [$findToken, $extraLeft, $extraRight] = $this->emptyListInsertionMap[$mapKey];
757: if ($findToken !== null) {
758: $originalTokensArray = $originalTokens->getTokens();
759: for (; $tokenIndex < count($originalTokensArray); $tokenIndex++) {
760: $result .= $originalTokensArray[$tokenIndex][Lexer::VALUE_OFFSET];
761: if ($originalTokensArray[$tokenIndex][Lexer::VALUE_OFFSET] !== $findToken) {
762: continue;
763: }
764:
765: $tokenIndex++;
766: break;
767: }
768: }
769: $first = true;
770: $result .= $extraLeft;
771: foreach ($delayedAdd as $delayedAddNode) {
772: if (!$first) {
773: $result .= $insertStr;
774: if ($insertNewline) {
775: $result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
776: }
777: }
778:
779: $result .= $this->printNodeFormatPreserving($delayedAddNode, $originalTokens);
780: $first = false;
781: }
782: $result .= $extraRight;
783: }
784:
785: return $result;
786: }
787:
788: /**
789: * @param list<Comment> $comments
790: */
791: private function printComments(array $comments, string $beforeAsteriskIndent, string $afterAsteriskIndent): string
792: {
793: $formattedComments = [];
794:
795: foreach ($comments as $comment) {
796: $formattedComments[] = str_replace("\n", "\n" . $beforeAsteriskIndent . '*' . $afterAsteriskIndent, $comment->getReformattedText());
797: }
798:
799: return implode("\n$beforeAsteriskIndent*$afterAsteriskIndent", $formattedComments);
800: }
801:
802: /**
803: * @param array<Node|null> $nodes
804: * @return array{bool, string, string}
805: */
806: private function isMultiline(int $initialIndex, array $nodes, TokenIterator $originalTokens): array
807: {
808: $isMultiline = count($nodes) > 1;
809: $pos = $initialIndex;
810: $allText = '';
811: /** @var Node|null $node */
812: foreach ($nodes as $node) {
813: if (!$node instanceof Node) {
814: continue;
815: }
816:
817: $endPos = $node->getAttribute(Attribute::END_INDEX) + 1;
818: $text = $originalTokens->getContentBetween($pos, $endPos);
819: $allText .= $text;
820: if (strpos($text, "\n") === false) {
821: // We require that a newline is present between *every* item. If the formatting
822: // is inconsistent, with only some items having newlines, we don't consider it
823: // as multiline
824: $isMultiline = false;
825: }
826: $pos = $endPos;
827: }
828:
829: $c = preg_match_all('~\n(?<before>[\\x09\\x20]*)\*(?<after>\\x20*)~', $allText, $matches, PREG_SET_ORDER);
830: if ($c === 0) {
831: return [$isMultiline, ' ', ' '];
832: }
833:
834: $before = '';
835: $after = '';
836: foreach ($matches as $match) {
837: if (strlen($match['before']) > strlen($before)) {
838: $before = $match['before'];
839: }
840: if (strlen($match['after']) <= strlen($after)) {
841: continue;
842: }
843:
844: $after = $match['after'];
845: }
846:
847: $before = strlen($before) === 0 ? ' ' : $before;
848: $after = strlen($after) === 0 ? ' ' : $after;
849:
850: return [$isMultiline, $before, $after];
851: }
852:
853: private function printNodeFormatPreserving(Node $node, TokenIterator $originalTokens): string
854: {
855: /** @var Node|null $originalNode */
856: $originalNode = $node->getAttribute(Attribute::ORIGINAL_NODE);
857: if ($originalNode === null) {
858: return $this->print($node);
859: }
860:
861: $class = get_class($node);
862: if ($class !== get_class($originalNode)) {
863: throw new LogicException();
864: }
865:
866: $startPos = $originalNode->getAttribute(Attribute::START_INDEX);
867: $endPos = $originalNode->getAttribute(Attribute::END_INDEX);
868: if ($startPos < 0 || $endPos < 0) {
869: throw new LogicException();
870: }
871:
872: $result = '';
873: $pos = $startPos;
874: $subNodeNames = array_keys(get_object_vars($node));
875: foreach ($subNodeNames as $subNodeName) {
876: $subNode = $node->$subNodeName;
877: $origSubNode = $originalNode->$subNodeName;
878:
879: if (
880: (!$subNode instanceof Node && $subNode !== null)
881: || (!$origSubNode instanceof Node && $origSubNode !== null)
882: ) {
883: if ($subNode === $origSubNode) {
884: // Unchanged, can reuse old code
885: continue;
886: }
887:
888: if (is_array($subNode) && is_array($origSubNode)) {
889: // Array subnode changed, we might be able to reconstruct it
890: $listResult = $this->printArrayFormatPreserving(
891: $subNode,
892: $origSubNode,
893: $originalTokens,
894: $pos,
895: $class,
896: $subNodeName,
897: );
898:
899: if ($listResult === null) {
900: return $this->print($node);
901: }
902:
903: $result .= $listResult;
904: continue;
905: }
906:
907: return $this->print($node);
908: }
909:
910: if ($origSubNode === null) {
911: if ($subNode === null) {
912: // Both null, nothing to do
913: continue;
914: }
915:
916: return $this->print($node);
917: }
918:
919: $subStartPos = $origSubNode->getAttribute(Attribute::START_INDEX);
920: $subEndPos = $origSubNode->getAttribute(Attribute::END_INDEX);
921: if ($subStartPos < 0 || $subEndPos < 0) {
922: throw new LogicException();
923: }
924:
925: if ($subEndPos < $subStartPos) {
926: return $this->print($node);
927: }
928:
929: if ($subNode === null) {
930: return $this->print($node);
931: }
932:
933: $result .= $originalTokens->getContentBetween($pos, $subStartPos);
934: $mapKey = get_class($node) . '->' . $subNodeName;
935: $parenthesesNeeded = isset($this->parenthesesMap[$mapKey])
936: && in_array(get_class($subNode), $this->parenthesesMap[$mapKey], true);
937:
938: if ($subNode->getAttribute(Attribute::ORIGINAL_NODE) !== null) {
939: $parenthesesNeeded = $parenthesesNeeded
940: && !in_array(get_class($subNode->getAttribute(Attribute::ORIGINAL_NODE)), $this->parenthesesMap[$mapKey], true);
941: }
942:
943: $addParentheses = $parenthesesNeeded && !$originalTokens->hasParentheses($subStartPos, $subEndPos);
944: if ($addParentheses) {
945: $result .= '(';
946: }
947:
948: $result .= $this->printNodeFormatPreserving($subNode, $originalTokens);
949: if ($addParentheses) {
950: $result .= ')';
951: }
952:
953: $pos = $subEndPos + 1;
954: }
955:
956: return $result . $originalTokens->getContentBetween($pos, $endPos + 1);
957: }
958:
959: }
960: