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