FPPP: Support anonymous classes (#432)

This is a huge hack... We temporarily create a new node with the
correct structure and use that for printing.

I think it would be better to always use a separate node type for
NewAnonClass, rather than using a combination of New and Class,
but this would require some larger changes, as this node type would
have to be both Expr and ClassLike, which is not possible right now,
as the latter is a class rather than an interface...
This commit is contained in:
Nikita Popov 2017-10-29 12:25:13 +01:00
parent 776275361a
commit 5900d78cc9
5 changed files with 90 additions and 14 deletions

View File

@ -3,7 +3,8 @@ Version 4.0.0-dev
### Fixed
* Added support for changing modifiers to formating-preserving pretty printer.
* Added support for changing modifiers to formatting-preserving pretty printer.
* Added support for anonymous classes to formatting-preserving pretty printer.
Version 4.0.0-alpha1 (2017-10-18)
---------------------------------

View File

@ -0,0 +1,55 @@
<?php
namespace PhpParser\Internal;
use PhpParser\Node;
use PhpParser\Node\Expr;
/**
* This node is used internally by the format-preserving pretty printer to print anonymous classes.
*
* The normal anonymous class structure violates assumptions about the order of token offsets.
* Namely, the constructor arguments are part of the Expr\New_ node and follow the class node, even
* though they are actually interleaved with them. This special node type is used temporarily to
* restore a sane token offset order.
*
* @internal
*/
class PrintableNewAnonClassNode extends Expr {
/** @var Node\Arg[] Arguments */
public $args;
/** @var null|Node\Name Name of extended class */
public $extends;
/** @var Node\Name[] Names of implemented interfaces */
public $implements;
/** @var Node\Stmt[] Statements */
public $stmts;
public function __construct(
array $args, Node\Name $extends = null, array $implements, array $stmts, array $attributes
) {
parent::__construct($attributes);
$this->args = $args;
$this->extends = $extends;
$this->implements = $implements;
$this->stmts = $stmts;
}
public static function fromNewNode(Expr\New_ $newNode) {
$class = $newNode->class;
assert($class instanceof Node\Stmt\Class_);
assert($class->name === null);
return new self(
$newNode->args, $class->extends, $class->implements,
$class->stmts, $newNode->getAttributes()
);
}
public function getType(): string {
return 'Expr_PrintableNewAnonClass';
}
public function getSubNodeNames() : array {
return ['args', 'extends', 'implements', 'stmts'];
}
}

View File

@ -3,7 +3,9 @@
namespace PhpParser;
use PhpParser\Internal\DiffElem;
use PhpParser\Internal\PrintableNewAnonClassNode;
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar;
use PhpParser\Node\Stmt;
@ -529,10 +531,11 @@ abstract class PrettyPrinterAbstract
return $this->pFallback($node);
}
$fallbackNode = $node;
if ($node instanceof Expr\New_ && $node->class instanceof Stmt\Class_) {
// For anonymous classes the new and class nodes are intermixed, this would require
// special handling (or maybe a new node type just for this?)
return $this->pFallback($node);
// Normalize node structure of anonymous classes
$node = PrintableNewAnonClassNode::fromNewNode($node);
$origNode = PrintableNewAnonClassNode::fromNewNode($origNode);
}
$indentAdjustment = $this->indentLevel - $this->origTokens->getIndentationBefore($startPos);
@ -562,7 +565,7 @@ abstract class PrettyPrinterAbstract
$this->listInsertionMap[$type . '->' . $subNodeName] ?? null
);
if (null === $listResult) {
return $this->pFallback($node);
return $this->pFallback($fallbackNode);
}
$result .= $listResult;
@ -573,7 +576,7 @@ abstract class PrettyPrinterAbstract
// Check if this is a modifier change
$key = $type . '->' . $subNodeName;
if (!isset($this->modifierChangeMap[$key])) {
return $this->pFallback($node);
return $this->pFallback($fallbackNode);
}
$findToken = $this->modifierChangeMap[$key];
@ -585,7 +588,7 @@ abstract class PrettyPrinterAbstract
// If a non-node, non-array subnode changed, we don't be able to do a partial
// reconstructions, as we don't have enough offset information. Pretty print the
// whole node instead.
return $this->pFallback($node);
return $this->pFallback($fallbackNode);
}
$extraLeft = '';
@ -595,7 +598,7 @@ abstract class PrettyPrinterAbstract
$subEndPos = $origSubNode->getEndTokenPos();
if ($subStartPos < 0 || $subEndPos < 0) {
// Shouldn't happen
return $this->pFallback($node);
return $this->pFallback($fallbackNode);
}
} else {
if ($subNode === null) {
@ -606,7 +609,7 @@ abstract class PrettyPrinterAbstract
// A node has been inserted, check if we have insertion information for it
$key = $type . '->' . $subNodeName;
if (!isset($this->insertionMap[$key])) {
return $this->pFallback($node);
return $this->pFallback($fallbackNode);
}
list($findToken, $extraLeft, $extraRight) = $this->insertionMap[$key];
@ -626,7 +629,7 @@ abstract class PrettyPrinterAbstract
// A node has been removed, check if we have removal information for it
$key = $type . '->' . $subNodeName;
if (!isset($this->removalMap[$key])) {
return $this->pFallback($node);
return $this->pFallback($fallbackNode);
}
// Adjust positions to account for additional tokens that must be skipped
@ -1069,6 +1072,7 @@ abstract class PrettyPrinterAbstract
'Stmt_Break->num' => $stripBoth,
'Stmt_ClassMethod->returnType' => $stripColon,
'Stmt_Class->extends' => ['left' => T_EXTENDS],
'Expr_PrintableNewAnonClass->extends' => ['left' => T_EXTENDS],
'Stmt_Continue->num' => $stripBoth,
'Stmt_Foreach->keyVar' => $stripDoubleArrow,
'Stmt_Function->returnType' => $stripColon,
@ -1102,6 +1106,7 @@ abstract class PrettyPrinterAbstract
'Stmt_Break->num' => [T_BREAK, ' ', null],
'Stmt_ClassMethod->returnType' => [')', ' : ', null],
'Stmt_Class->extends' => [null, ' extends ', null],
'Expr_PrintableNewAnonClass->extends' => [null, ' extends ', null],
'Stmt_Continue->num' => [T_CONTINUE, ' ', null],
'Stmt_Foreach->keyVar' => [T_AS, null, ' => '],
'Stmt_Function->returnType' => [')', ' : ', null],
@ -1141,10 +1146,12 @@ abstract class PrettyPrinterAbstract
'Expr_List->items' => ', ',
'Expr_MethodCall->args' => ', ',
'Expr_New->args' => ', ',
'Expr_PrintableNewAnonClass->args' => ', ',
'Expr_StaticCall->args' => ', ',
'Stmt_ClassConst->consts' => ', ',
'Stmt_ClassMethod->params' => ', ',
'Stmt_Class->implements' => ', ',
'Expr_PrintableNewAnonClass->implements' => ', ',
'Stmt_Const->consts' => ', ',
'Stmt_Declare->declares' => ', ',
'Stmt_Echo->exprs' => ', ',
@ -1167,6 +1174,7 @@ abstract class PrettyPrinterAbstract
'Stmt_Case->stmts' => "\n",
'Stmt_Catch->stmts' => "\n",
'Stmt_Class->stmts' => "\n",
'Expr_PrintableNewAnonClass->stmts' => "\n",
'Stmt_Interface->stmts' => "\n",
'Stmt_Trait->stmts' => "\n",
'Stmt_ClassMethod->stmts' => "\n",

View File

@ -261,10 +261,6 @@ CODE
* This test makes sure that the format-preserving pretty printer round-trips for all
* the pretty printer tests (i.e. returns the input if no changes occurred).
*/
if (false !== strpos($code, 'new class')) {
// Can't preserve formatting on anon classes for now
return;
}
list($version) = $this->parseModeLine($modeLine);

View File

@ -0,0 +1,16 @@
Anonymous classes
-----
<?php
new class
($x)
extends X
{ };
-----
$new = $stmts[0]->expr;
$new->class->extends = null;
$new->args[] = new Expr\Variable('y');
-----
<?php
new class
($x, $y)
{ };