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