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