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