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