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