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