diff --git a/src/Parse/PHP/ArrayPrinter.php b/src/Parse/PHP/ArrayPrinter.php index 3fd87db39..2d3b89d24 100644 --- a/src/Parse/PHP/ArrayPrinter.php +++ b/src/Parse/PHP/ArrayPrinter.php @@ -1,5 +1,7 @@ lexer = $lexer; + + $p = "prettyPrint($stmts); + + if ($stmts[0] instanceof Stmt\InlineHTML) { + $p = preg_replace('/^<\?php\s+\?>\n?/', '', $p); + } + if ($stmts[count($stmts) - 1] instanceof Stmt\InlineHTML) { + $p = preg_replace('/<\?php$/', '', rtrim($p)); + } + + $this->lexer = null; + + return $p; + } + /** * @param array $nodes * @param bool $trailingComma @@ -48,6 +94,7 @@ protected function pCommaSeparatedMultiline(array $nodes, bool $trailingComma) : foreach ($nodes as $idx => $node) { if ($node !== null) { $comments = $node->getComments(); + if ($comments) { $result .= $this->pComments($comments); } @@ -65,10 +112,175 @@ protected function pCommaSeparatedMultiline(array $nodes, bool $trailingComma) : return $result; } + /** + * Render an array expression + * + * @param Expr\Array_ $node Array expression node + * + * @return string Comma separated pretty printed nodes in multiline style + */ + protected function pExpr_Array(Expr\Array_ $node): string + { + $default = $this->options['shortArraySyntax'] + ? Expr\Array_::KIND_SHORT + : Expr\Array_::KIND_LONG; + + $ops = $node->getAttribute('kind', $default) === Expr\Array_::KIND_SHORT + ? ['[', ']'] + : ['array(', ')']; + + if (!count($node->items) && $comments = $this->getNodeComments($node)) { + // the array has no items, we can inject whatever we want + return sprintf( + '%s%s%s%s%s', + // opening control char + $ops[0], + // indent and add nl string + $this->indent(), + // join all comments with nl string + implode($this->nl, $comments), + // outdent and add nl string + $this->outdent(), + // closing control char + $ops[1] + ); + } + + if ($comments = $this->getCommentsNotInArray($node)) { + // array has items, we have detected comments not included within the array, therefore we have found + // trailing comments and must append them to the end of the array + return sprintf( + '%s%s%s%s%s%s', + // opening control char + $ops[0], + // render the children + $this->pMaybeMultiline($node->items, true), + // add 1 level of indentation + str_repeat(' ', 4), + // join all comments with the current indentation + implode($this->nl . str_repeat(' ', 4), $comments), + // add a trailing nl + $this->nl, + // closing control char + $ops[1] + ); + } + + // default return + return $ops[0] . $this->pMaybeMultiline($node->items, true) . $ops[1]; + } + + /** + * Increase indentation level. + * Proxied to allow for nl return + * + * @return string + */ + protected function indent(): string + { + $this->indentLevel += 4; + $this->nl .= ' '; + return $this->nl; + } + + /** + * Decrease indentation level. + * Proxied to allow for nl return + * + * @return string + */ + protected function outdent(): string + { + assert($this->indentLevel >= 4); + $this->indentLevel -= 4; + $this->nl = "\n" . str_repeat(' ', $this->indentLevel); + return $this->nl; + } + + /** + * Get all comments that have not been attributed to a node within a node array + * + * @param Expr\Array_ $nodes Array of nodes + * + * @return array Comments found + */ + protected function getCommentsNotInArray(Expr\Array_ $nodes): array + { + if (!$comments = $this->getNodeComments($nodes)) { + return []; + } + + return array_filter($comments, function ($comment) use ($nodes) { + return !$this->commentInNodeList($nodes->items, $comment); + }); + } + + /** + * Recursively check if a comment exists in an array of nodes + * + * @param Node[] $nodes Array of nodes + * @param string $comment The comment to search for + * + * @return bool + */ + protected function commentInNodeList(array $nodes, string $comment): bool + { + foreach ($nodes as $node) { + if ($node->value instanceof Expr\Array_ && $this->commentInNodeList($node->value->items, $comment)) { + return true; + } + if ($nodeComments = $node->getAttribute('comments')) { + foreach ($nodeComments as $nodeComment) { + if ($nodeComment->getText() === $comment) { + return true; + } + } + } + } + + return false; + } + + /** + * Check the lexer tokens for comments within the node's start & end position + * + * @param Node $node Node to check + * + * @return ?array + */ + protected function getNodeComments(Node $node): ?array + { + $tokens = $this->lexer->getTokens(); + $pos = $node->getAttribute('startTokenPos'); + $end = $node->getAttribute('endTokenPos'); + $endLine = $node->getAttribute('endLine'); + $content = []; + + while (++$pos < $end) { + if (!isset($tokens[$pos]) || !is_array($tokens[$pos]) || $tokens[$pos][0] === T_WHITESPACE) { + continue; + } + + list($type, $string, $line) = $tokens[$pos]; + + if ($line > $endLine) { + break; + } + + if ($type === T_COMMENT || $type === T_DOC_COMMENT) { + $content[] = $string; + } elseif ($content) { + return $content; + } + } + + return empty($content) ? null : $content; + } + /** * Prints reformatted text of the passed comments. * - * @param Comment[] $comments List of comments + * @param array $comments List of comments * * @return string Reformatted text of comments */