Chris@0: array( 0, 1), Chris@0: 'Expr_BitwiseNot' => array( 10, 1), Chris@0: 'Expr_PreInc' => array( 10, 1), Chris@0: 'Expr_PreDec' => array( 10, 1), Chris@0: 'Expr_PostInc' => array( 10, -1), Chris@0: 'Expr_PostDec' => array( 10, -1), Chris@0: 'Expr_UnaryPlus' => array( 10, 1), Chris@0: 'Expr_UnaryMinus' => array( 10, 1), Chris@0: 'Expr_Cast_Int' => array( 10, 1), Chris@0: 'Expr_Cast_Double' => array( 10, 1), Chris@0: 'Expr_Cast_String' => array( 10, 1), Chris@0: 'Expr_Cast_Array' => array( 10, 1), Chris@0: 'Expr_Cast_Object' => array( 10, 1), Chris@0: 'Expr_Cast_Bool' => array( 10, 1), Chris@0: 'Expr_Cast_Unset' => array( 10, 1), Chris@0: 'Expr_ErrorSuppress' => array( 10, 1), Chris@0: 'Expr_Instanceof' => array( 20, 0), Chris@0: 'Expr_BooleanNot' => array( 30, 1), Chris@0: 'Expr_BinaryOp_Mul' => array( 40, -1), Chris@0: 'Expr_BinaryOp_Div' => array( 40, -1), Chris@0: 'Expr_BinaryOp_Mod' => array( 40, -1), Chris@0: 'Expr_BinaryOp_Plus' => array( 50, -1), Chris@0: 'Expr_BinaryOp_Minus' => array( 50, -1), Chris@0: 'Expr_BinaryOp_Concat' => array( 50, -1), Chris@0: 'Expr_BinaryOp_ShiftLeft' => array( 60, -1), Chris@0: 'Expr_BinaryOp_ShiftRight' => array( 60, -1), Chris@0: 'Expr_BinaryOp_Smaller' => array( 70, 0), Chris@0: 'Expr_BinaryOp_SmallerOrEqual' => array( 70, 0), Chris@0: 'Expr_BinaryOp_Greater' => array( 70, 0), Chris@0: 'Expr_BinaryOp_GreaterOrEqual' => array( 70, 0), Chris@0: 'Expr_BinaryOp_Equal' => array( 80, 0), Chris@0: 'Expr_BinaryOp_NotEqual' => array( 80, 0), Chris@0: 'Expr_BinaryOp_Identical' => array( 80, 0), Chris@0: 'Expr_BinaryOp_NotIdentical' => array( 80, 0), Chris@0: 'Expr_BinaryOp_Spaceship' => array( 80, 0), Chris@0: 'Expr_BinaryOp_BitwiseAnd' => array( 90, -1), Chris@0: 'Expr_BinaryOp_BitwiseXor' => array(100, -1), Chris@0: 'Expr_BinaryOp_BitwiseOr' => array(110, -1), Chris@0: 'Expr_BinaryOp_BooleanAnd' => array(120, -1), Chris@0: 'Expr_BinaryOp_BooleanOr' => array(130, -1), Chris@0: 'Expr_BinaryOp_Coalesce' => array(140, 1), Chris@0: 'Expr_Ternary' => array(150, -1), Chris@0: // parser uses %left for assignments, but they really behave as %right Chris@0: 'Expr_Assign' => array(160, 1), Chris@0: 'Expr_AssignRef' => array(160, 1), Chris@0: 'Expr_AssignOp_Plus' => array(160, 1), Chris@0: 'Expr_AssignOp_Minus' => array(160, 1), Chris@0: 'Expr_AssignOp_Mul' => array(160, 1), Chris@0: 'Expr_AssignOp_Div' => array(160, 1), Chris@0: 'Expr_AssignOp_Concat' => array(160, 1), Chris@0: 'Expr_AssignOp_Mod' => array(160, 1), Chris@0: 'Expr_AssignOp_BitwiseAnd' => array(160, 1), Chris@0: 'Expr_AssignOp_BitwiseOr' => array(160, 1), Chris@0: 'Expr_AssignOp_BitwiseXor' => array(160, 1), Chris@0: 'Expr_AssignOp_ShiftLeft' => array(160, 1), Chris@0: 'Expr_AssignOp_ShiftRight' => array(160, 1), Chris@0: 'Expr_AssignOp_Pow' => array(160, 1), Chris@0: 'Expr_YieldFrom' => array(165, 1), Chris@0: 'Expr_Print' => array(168, 1), Chris@0: 'Expr_BinaryOp_LogicalAnd' => array(170, -1), Chris@0: 'Expr_BinaryOp_LogicalXor' => array(180, -1), Chris@0: 'Expr_BinaryOp_LogicalOr' => array(190, -1), Chris@0: 'Expr_Include' => array(200, -1), Chris@0: ); Chris@0: Chris@0: protected $noIndentToken; Chris@0: protected $docStringEndToken; Chris@0: protected $canUseSemicolonNamespaces; Chris@0: protected $options; Chris@0: Chris@0: /** Chris@0: * Creates a pretty printer instance using the given options. Chris@0: * Chris@0: * Supported options: Chris@0: * * bool $shortArraySyntax = false: Whether to use [] instead of array() as the default array Chris@0: * syntax, if the node does not specify a format. Chris@0: * Chris@0: * @param array $options Dictionary of formatting options Chris@0: */ Chris@0: public function __construct(array $options = []) { Chris@0: $this->noIndentToken = '_NO_INDENT_' . mt_rand(); Chris@0: $this->docStringEndToken = '_DOC_STRING_END_' . mt_rand(); Chris@0: Chris@0: $defaultOptions = ['shortArraySyntax' => false]; Chris@0: $this->options = $options + $defaultOptions; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Pretty prints an array of statements. Chris@0: * Chris@0: * @param Node[] $stmts Array of statements Chris@0: * Chris@0: * @return string Pretty printed statements Chris@0: */ Chris@0: public function prettyPrint(array $stmts) { Chris@0: $this->preprocessNodes($stmts); Chris@0: Chris@0: return ltrim($this->handleMagicTokens($this->pStmts($stmts, false))); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Pretty prints an expression. Chris@0: * Chris@0: * @param Expr $node Expression node Chris@0: * Chris@0: * @return string Pretty printed node Chris@0: */ Chris@0: public function prettyPrintExpr(Expr $node) { Chris@0: return $this->handleMagicTokens($this->p($node)); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Pretty prints a file of statements (includes the opening prettyPrint($stmts); Chris@0: Chris@0: if ($stmts[0] instanceof Stmt\InlineHTML) { Chris@0: $p = preg_replace('/^<\?php\s+\?>\n?/', '', $p); Chris@0: } Chris@0: if ($stmts[count($stmts) - 1] instanceof Stmt\InlineHTML) { Chris@0: $p = preg_replace('/<\?php$/', '', rtrim($p)); Chris@0: } Chris@0: Chris@0: return $p; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Preprocesses the top-level nodes to initialize pretty printer state. Chris@0: * Chris@0: * @param Node[] $nodes Array of nodes Chris@0: */ Chris@0: protected function preprocessNodes(array $nodes) { Chris@0: /* We can use semicolon-namespaces unless there is a global namespace declaration */ Chris@0: $this->canUseSemicolonNamespaces = true; Chris@0: foreach ($nodes as $node) { Chris@0: if ($node instanceof Stmt\Namespace_ && null === $node->name) { Chris@0: $this->canUseSemicolonNamespaces = false; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: protected function handleMagicTokens($str) { Chris@0: // Drop no-indent tokens Chris@0: $str = str_replace($this->noIndentToken, '', $str); Chris@0: Chris@0: // Replace doc-string-end tokens with nothing or a newline Chris@0: $str = str_replace($this->docStringEndToken . ";\n", ";\n", $str); Chris@0: $str = str_replace($this->docStringEndToken, "\n", $str); Chris@0: Chris@0: return $str; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Pretty prints an array of nodes (statements) and indents them optionally. Chris@0: * Chris@0: * @param Node[] $nodes Array of nodes Chris@0: * @param bool $indent Whether to indent the printed nodes Chris@0: * Chris@0: * @return string Pretty printed statements Chris@0: */ Chris@0: protected function pStmts(array $nodes, $indent = true) { Chris@0: $result = ''; Chris@0: foreach ($nodes as $node) { Chris@0: $comments = $node->getAttribute('comments', array()); Chris@0: if ($comments) { Chris@0: $result .= "\n" . $this->pComments($comments); Chris@0: if ($node instanceof Stmt\Nop) { Chris@0: continue; Chris@0: } Chris@0: } Chris@0: Chris@0: $result .= "\n" . $this->p($node) . ($node instanceof Expr ? ';' : ''); Chris@0: } Chris@0: Chris@0: if ($indent) { Chris@0: return preg_replace('~\n(?!$|' . $this->noIndentToken . ')~', "\n ", $result); Chris@0: } else { Chris@0: return $result; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Pretty prints a node. Chris@0: * Chris@0: * @param Node $node Node to be pretty printed Chris@0: * Chris@0: * @return string Pretty printed node Chris@0: */ Chris@0: protected function p(Node $node) { Chris@0: return $this->{'p' . $node->getType()}($node); Chris@0: } Chris@0: Chris@0: protected function pInfixOp($type, Node $leftNode, $operatorString, Node $rightNode) { Chris@0: list($precedence, $associativity) = $this->precedenceMap[$type]; Chris@0: Chris@0: return $this->pPrec($leftNode, $precedence, $associativity, -1) Chris@0: . $operatorString Chris@0: . $this->pPrec($rightNode, $precedence, $associativity, 1); Chris@0: } Chris@0: Chris@0: protected function pPrefixOp($type, $operatorString, Node $node) { Chris@0: list($precedence, $associativity) = $this->precedenceMap[$type]; Chris@0: return $operatorString . $this->pPrec($node, $precedence, $associativity, 1); Chris@0: } Chris@0: Chris@0: protected function pPostfixOp($type, Node $node, $operatorString) { Chris@0: list($precedence, $associativity) = $this->precedenceMap[$type]; Chris@0: return $this->pPrec($node, $precedence, $associativity, -1) . $operatorString; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Prints an expression node with the least amount of parentheses necessary to preserve the meaning. Chris@0: * Chris@0: * @param Node $node Node to pretty print Chris@0: * @param int $parentPrecedence Precedence of the parent operator Chris@0: * @param int $parentAssociativity Associativity of parent operator Chris@0: * (-1 is left, 0 is nonassoc, 1 is right) Chris@0: * @param int $childPosition Position of the node relative to the operator Chris@0: * (-1 is left, 1 is right) Chris@0: * Chris@0: * @return string The pretty printed node Chris@0: */ Chris@0: protected function pPrec(Node $node, $parentPrecedence, $parentAssociativity, $childPosition) { Chris@0: $type = $node->getType(); Chris@0: if (isset($this->precedenceMap[$type])) { Chris@0: $childPrecedence = $this->precedenceMap[$type][0]; Chris@0: if ($childPrecedence > $parentPrecedence Chris@0: || ($parentPrecedence == $childPrecedence && $parentAssociativity != $childPosition) Chris@0: ) { Chris@0: return '(' . $this->p($node) . ')'; Chris@0: } Chris@0: } Chris@0: Chris@0: return $this->p($node); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Pretty prints an array of nodes and implodes the printed values. Chris@0: * Chris@0: * @param Node[] $nodes Array of Nodes to be printed Chris@0: * @param string $glue Character to implode with Chris@0: * Chris@0: * @return string Imploded pretty printed nodes Chris@0: */ Chris@0: protected function pImplode(array $nodes, $glue = '') { Chris@0: $pNodes = array(); Chris@0: foreach ($nodes as $node) { Chris@0: if (null === $node) { Chris@0: $pNodes[] = ''; Chris@0: } else { Chris@0: $pNodes[] = $this->p($node); Chris@0: } Chris@0: } Chris@0: Chris@0: return implode($glue, $pNodes); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Pretty prints an array of nodes and implodes the printed values with commas. Chris@0: * Chris@0: * @param Node[] $nodes Array of Nodes to be printed Chris@0: * Chris@0: * @return string Comma separated pretty printed nodes Chris@0: */ Chris@0: protected function pCommaSeparated(array $nodes) { Chris@0: return $this->pImplode($nodes, ', '); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Pretty prints a comma-separated list of nodes in multiline style, including comments. Chris@0: * Chris@0: * The result includes a leading newline and one level of indentation (same as pStmts). Chris@0: * Chris@0: * @param Node[] $nodes Array of Nodes to be printed Chris@0: * @param bool $trailingComma Whether to use a trailing comma Chris@0: * Chris@0: * @return string Comma separated pretty printed nodes in multiline style Chris@0: */ Chris@0: protected function pCommaSeparatedMultiline(array $nodes, $trailingComma) { Chris@0: $result = ''; Chris@0: $lastIdx = count($nodes) - 1; Chris@0: foreach ($nodes as $idx => $node) { Chris@0: if ($node !== null) { Chris@0: $comments = $node->getAttribute('comments', array()); Chris@0: if ($comments) { Chris@0: $result .= "\n" . $this->pComments($comments); Chris@0: } Chris@0: Chris@0: $result .= "\n" . $this->p($node); Chris@0: } else { Chris@0: $result .= "\n"; Chris@0: } Chris@0: if ($trailingComma || $idx !== $lastIdx) { Chris@0: $result .= ','; Chris@0: } Chris@0: } Chris@0: Chris@0: return preg_replace('~\n(?!$|' . $this->noIndentToken . ')~', "\n ", $result); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Signals the pretty printer that a string shall not be indented. Chris@0: * Chris@0: * @param string $string Not to be indented string Chris@0: * Chris@0: * @return string String marked with $this->noIndentToken's. Chris@0: */ Chris@0: protected function pNoIndent($string) { Chris@0: return str_replace("\n", "\n" . $this->noIndentToken, $string); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Prints reformatted text of the passed comments. Chris@0: * Chris@0: * @param Comment[] $comments List of comments Chris@0: * Chris@0: * @return string Reformatted text of comments Chris@0: */ Chris@0: protected function pComments(array $comments) { Chris@0: $formattedComments = []; Chris@0: Chris@0: foreach ($comments as $comment) { Chris@0: $formattedComments[] = $comment->getReformattedText(); Chris@0: } Chris@0: Chris@0: return implode("\n", $formattedComments); Chris@0: } Chris@0: }