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