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