emptyPhpDocDetector = $emptyPhpDocDetector; $this->docBlockInliner = $docBlockInliner; $this->removeNodesStartAndEndResolver = $removeNodesStartAndEndResolver; $this->changedPhpDocNodeVisitor = $changedPhpDocNodeVisitor; $changedPhpDocNodeTraverser = new PhpDocNodeTraverser(); $changedPhpDocNodeTraverser->addPhpDocNodeVisitor($this->changedPhpDocNodeVisitor); $this->changedPhpDocNodeTraverser = $changedPhpDocNodeTraverser; } public function printNew(PhpDocInfo $phpDocInfo) : string { $docContent = (string) $phpDocInfo->getPhpDocNode(); if ($phpDocInfo->isSingleLine()) { return $this->docBlockInliner->inline($docContent); } if ($phpDocInfo->getNode() instanceof InlineHTML) { return ''; } return $docContent; } /** * As in php-parser * * ref: https://github.com/nikic/PHP-Parser/issues/487#issuecomment-375986259 * - Tokens[node.startPos .. subnode1.startPos] * - Print(subnode1) * - Tokens[subnode1.endPos .. subnode2.startPos] * - Print(subnode2) * - Tokens[subnode2.endPos .. node.endPos] */ public function printFormatPreserving(PhpDocInfo $phpDocInfo) : string { if ($phpDocInfo->getTokens() === []) { // completely new one, just print string version of it if ($phpDocInfo->getPhpDocNode()->children === []) { return ''; } if ($phpDocInfo->getNode() instanceof InlineHTML) { return 'getPhpDocNode() . \PHP_EOL . '?>'; } return (string) $phpDocInfo->getPhpDocNode(); } $phpDocNode = $phpDocInfo->getPhpDocNode(); $this->tokens = $phpDocInfo->getTokens(); $this->tokenCount = $phpDocInfo->getTokenCount(); $this->phpDocInfo = $phpDocInfo; $this->currentTokenPosition = 0; $phpDocString = $this->printPhpDocNode($phpDocNode); // hotfix of extra space with callable () return Strings::replace($phpDocString, self::CALLABLE_REGEX, 'callable('); } private function getCurrentPhpDocInfo() : PhpDocInfo { if (!$this->phpDocInfo instanceof PhpDocInfo) { throw new ShouldNotHappenException(); } return $this->phpDocInfo; } private function printPhpDocNode(PhpDocNode $phpDocNode) : string { // no nodes were, so empty doc if ($this->emptyPhpDocDetector->isPhpDocNodeEmpty($phpDocNode)) { return ''; } $output = ''; // node output $nodeCount = \count($phpDocNode->children); foreach ($phpDocNode->children as $key => $phpDocChildNode) { $output .= $this->printDocChildNode($phpDocChildNode, $key + 1, $nodeCount); } $output = $this->printEnd($output); // fix missing start if (!StringUtils::isMatch($output, self::DOCBLOCK_START_REGEX) && $output !== '') { $output = '/**' . $output; } // fix missing end if (StringUtils::isMatch($output, self::OPENING_DOCBLOCK_REGEX) && !StringUtils::isMatch($output, self::CLOSING_DOCBLOCK_REGEX)) { $output .= ' */'; } return $output; } private function printDocChildNode(PhpDocChildNode $phpDocChildNode, int $key = 0, int $nodeCount = 0) : string { $output = ''; $shouldReprintChildNode = $this->shouldReprint($phpDocChildNode); if ($phpDocChildNode instanceof PhpDocTagNode && ($shouldReprintChildNode && ($phpDocChildNode->value instanceof ParamTagValueNode || $phpDocChildNode->value instanceof ThrowsTagValueNode || $phpDocChildNode->value instanceof VarTagValueNode || $phpDocChildNode->value instanceof ReturnTagValueNode || $phpDocChildNode->value instanceof PropertyTagValueNode))) { // the type has changed → reprint $phpDocChildNodeStartEnd = $phpDocChildNode->getAttribute(PhpDocAttributeKey::START_AND_END); // bump the last position of token after just printed node if ($phpDocChildNodeStartEnd instanceof StartAndEnd) { $this->currentTokenPosition = $phpDocChildNodeStartEnd->getEnd(); } return $this->standardPrintPhpDocChildNode($phpDocChildNode); } /** @var StartAndEnd|null $startAndEnd */ $startAndEnd = $phpDocChildNode->getAttribute(PhpDocAttributeKey::START_AND_END); if ($startAndEnd instanceof StartAndEnd && !$shouldReprintChildNode) { $isLastToken = $nodeCount === $key; // correct previously changed node $this->correctPreviouslyReprintedFirstNode($key, $startAndEnd); $output = $this->addTokensFromTo($output, $this->currentTokenPosition, $startAndEnd->getEnd(), $isLastToken); $this->currentTokenPosition = $startAndEnd->getEnd(); return \rtrim($output); } if ($startAndEnd instanceof StartAndEnd) { $this->currentTokenPosition = $startAndEnd->getEnd(); } $standardPrintedPhpDocChildNode = $this->standardPrintPhpDocChildNode($phpDocChildNode); return $output . $standardPrintedPhpDocChildNode; } private function printEnd(string $output) : string { $lastTokenPosition = $this->getCurrentPhpDocInfo()->getPhpDocNode()->getAttribute(PhpDocAttributeKey::LAST_PHP_DOC_TOKEN_POSITION); if ($lastTokenPosition === null) { $lastTokenPosition = $this->currentTokenPosition; } if ($lastTokenPosition === 0) { $lastTokenPosition = 1; } return $this->addTokensFromTo($output, $lastTokenPosition, $this->tokenCount, \true); } private function addTokensFromTo(string $output, int $from, int $to, bool $shouldSkipEmptyLinesAbove) : string { // skip removed nodes $positionJumpSet = []; $removedStartAndEnds = $this->removeNodesStartAndEndResolver->resolve($this->getCurrentPhpDocInfo()->getOriginalPhpDocNode(), $this->getCurrentPhpDocInfo()->getPhpDocNode(), $this->tokens); foreach ($removedStartAndEnds as $removedStartAndEnd) { $positionJumpSet[$removedStartAndEnd->getStart()] = $removedStartAndEnd->getEnd(); } // include also space before, in case of inlined docs if (isset($this->tokens[$from - 1]) && $this->tokens[$from - 1][1] === Lexer::TOKEN_HORIZONTAL_WS) { --$from; } // skip extra empty lines above if this is the last one if ($shouldSkipEmptyLinesAbove && \strpos((string) $this->tokens[$from][0], \PHP_EOL) !== \false && \strpos((string) $this->tokens[$from + 1][0], \PHP_EOL) !== \false) { ++$from; } return $this->appendToOutput($output, $from, $to, $positionJumpSet); } /** * @param array $positionJumpSet */ private function appendToOutput(string $output, int $from, int $to, array $positionJumpSet) : string { for ($i = $from; $i < $to; ++$i) { while (isset($positionJumpSet[$i])) { $i = $positionJumpSet[$i]; } $output .= $this->tokens[$i][0] ?? ''; } return $output; } private function correctPreviouslyReprintedFirstNode(int $key, StartAndEnd $startAndEnd) : void { if ($this->currentTokenPosition !== 0) { return; } if ($key === 1) { return; } $startTokenPosition = $startAndEnd->getStart(); $tokens = $this->getCurrentPhpDocInfo()->getTokens(); if (!isset($tokens[$startTokenPosition - 1])) { return; } $previousToken = $tokens[$startTokenPosition - 1]; if ($previousToken[1] === Lexer::TOKEN_PHPDOC_EOL) { --$startTokenPosition; } $this->currentTokenPosition = $startTokenPosition; } private function shouldReprint(PhpDocChildNode $phpDocChildNode) : bool { $this->changedPhpDocNodeTraverser->traverse($phpDocChildNode); return $this->changedPhpDocNodeVisitor->hasChanged(); } private function standardPrintPhpDocChildNode(PhpDocChildNode $phpDocChildNode) : string { $printedNode = (string) $phpDocChildNode; if ($this->getCurrentPhpDocInfo()->isSingleLine()) { return ' ' . $printedNode; } return self::NEWLINE_WITH_ASTERISK . ($printedNode === '' ? '' : ' ' . $printedNode); } }