changes for filter and print

This commit is contained in:
2024-09-29 16:59:27 +05:45
parent 497f567cba
commit 684e01bf48
1335 changed files with 38709 additions and 74987 deletions

View File

@ -0,0 +1,34 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast;
/**
* Inspired by https://github.com/nikic/PHP-Parser/tree/36a6dcd04e7b0285e8f0868f44bd4927802f7df1
*
* Copyright (c) 2011, Nikita Popov
* All rights reserved.
*/
abstract class AbstractNodeVisitor implements NodeVisitor // phpcs:ignore SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming.SuperfluousPrefix
{
public function beforeTraverse(array $nodes): ?array
{
return null;
}
public function enterNode(Node $node)
{
return null;
}
public function leaveNode(Node $node)
{
return null;
}
public function afterTraverse(array $nodes): ?array
{
return null;
}
}

View File

@ -0,0 +1,16 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast;
final class Attribute
{
public const START_LINE = 'startLine';
public const END_LINE = 'endLine';
public const START_INDEX = 'startIndex';
public const END_INDEX = 'endIndex';
public const ORIGINAL_NODE = 'originalNode';
}

View File

@ -0,0 +1,36 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;
class ConstExprArrayItemNode implements ConstExprNode
{
use NodeAttributes;
/** @var ConstExprNode|null */
public $key;
/** @var ConstExprNode */
public $value;
public function __construct(?ConstExprNode $key, ConstExprNode $value)
{
$this->key = $key;
$this->value = $value;
}
public function __toString(): string
{
if ($this->key !== null) {
return sprintf('%s => %s', $this->key, $this->value);
}
return (string) $this->value;
}
}

View File

@ -0,0 +1,30 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function implode;
class ConstExprArrayNode implements ConstExprNode
{
use NodeAttributes;
/** @var ConstExprArrayItemNode[] */
public $items;
/**
* @param ConstExprArrayItemNode[] $items
*/
public function __construct(array $items)
{
$this->items = $items;
}
public function __toString(): string
{
return '[' . implode(', ', $this->items) . ']';
}
}

View File

@ -0,0 +1,17 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ConstExprFalseNode implements ConstExprNode
{
use NodeAttributes;
public function __toString(): string
{
return 'false';
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ConstExprFloatNode implements ConstExprNode
{
use NodeAttributes;
/** @var string */
public $value;
public function __construct(string $value)
{
$this->value = $value;
}
public function __toString(): string
{
return $this->value;
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ConstExprIntegerNode implements ConstExprNode
{
use NodeAttributes;
/** @var string */
public $value;
public function __construct(string $value)
{
$this->value = $value;
}
public function __toString(): string
{
return $this->value;
}
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\Node;
interface ConstExprNode extends Node
{
}

View File

@ -0,0 +1,17 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ConstExprNullNode implements ConstExprNode
{
use NodeAttributes;
public function __toString(): string
{
return 'null';
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ConstExprStringNode implements ConstExprNode
{
use NodeAttributes;
/** @var string */
public $value;
public function __construct(string $value)
{
$this->value = $value;
}
public function __toString(): string
{
return $this->value;
}
}

View File

@ -0,0 +1,17 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ConstExprTrueNode implements ConstExprNode
{
use NodeAttributes;
public function __toString(): string
{
return 'true';
}
}

View File

@ -0,0 +1,35 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ConstFetchNode implements ConstExprNode
{
use NodeAttributes;
/** @var string class name for class constants or empty string for non-class constants */
public $className;
/** @var string */
public $name;
public function __construct(string $className, string $name)
{
$this->className = $className;
$this->name = $name;
}
public function __toString(): string
{
if ($this->className === '') {
return $this->name;
}
return "{$this->className}::{$this->name}";
}
}

View File

@ -0,0 +1,42 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;
use function str_replace;
use function strlen;
use function substr;
class DoctrineConstExprStringNode extends ConstExprStringNode
{
use NodeAttributes;
/** @var string */
public $value;
public function __construct(string $value)
{
parent::__construct($value);
$this->value = $value;
}
public function __toString(): string
{
return self::escape($this->value);
}
public static function unescape(string $value): string
{
// from https://github.com/doctrine/annotations/blob/a9ec7af212302a75d1f92fa65d3abfbd16245a2a/lib/Doctrine/Common/Annotations/DocLexer.php#L103-L107
return str_replace('""', '"', substr($value, 1, strlen($value) - 2));
}
private static function escape(string $value): string
{
// from https://github.com/phpstan/phpdoc-parser/issues/205#issuecomment-1662323656
return sprintf('"%s"', str_replace('"', '""', $value));
}
}

View File

@ -0,0 +1,78 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function addcslashes;
use function assert;
use function dechex;
use function ord;
use function preg_replace_callback;
use function sprintf;
use function str_pad;
use function strlen;
use const STR_PAD_LEFT;
class QuoteAwareConstExprStringNode extends ConstExprStringNode implements ConstExprNode
{
public const SINGLE_QUOTED = 1;
public const DOUBLE_QUOTED = 2;
use NodeAttributes;
/** @var self::SINGLE_QUOTED|self::DOUBLE_QUOTED */
public $quoteType;
/**
* @param self::SINGLE_QUOTED|self::DOUBLE_QUOTED $quoteType
*/
public function __construct(string $value, int $quoteType)
{
parent::__construct($value);
$this->quoteType = $quoteType;
}
public function __toString(): string
{
if ($this->quoteType === self::SINGLE_QUOTED) {
// from https://github.com/nikic/PHP-Parser/blob/0ffddce52d816f72d0efc4d9b02e276d3309ef01/lib/PhpParser/PrettyPrinter/Standard.php#L1007
return sprintf("'%s'", addcslashes($this->value, '\'\\'));
}
// from https://github.com/nikic/PHP-Parser/blob/0ffddce52d816f72d0efc4d9b02e276d3309ef01/lib/PhpParser/PrettyPrinter/Standard.php#L1010-L1040
return sprintf('"%s"', $this->escapeDoubleQuotedString());
}
private function escapeDoubleQuotedString(): string
{
$quote = '"';
$escaped = addcslashes($this->value, "\n\r\t\f\v$" . $quote . '\\');
// Escape control characters and non-UTF-8 characters.
// Regex based on https://stackoverflow.com/a/11709412/385378.
$regex = '/(
[\x00-\x08\x0E-\x1F] # Control characters
| [\xC0-\xC1] # Invalid UTF-8 Bytes
| [\xF5-\xFF] # Invalid UTF-8 Bytes
| \xE0(?=[\x80-\x9F]) # Overlong encoding of prior code point
| \xF0(?=[\x80-\x8F]) # Overlong encoding of prior code point
| [\xC2-\xDF](?![\x80-\xBF]) # Invalid UTF-8 Sequence Start
| [\xE0-\xEF](?![\x80-\xBF]{2}) # Invalid UTF-8 Sequence Start
| [\xF0-\xF4](?![\x80-\xBF]{3}) # Invalid UTF-8 Sequence Start
| (?<=[\x00-\x7F\xF5-\xFF])[\x80-\xBF] # Invalid UTF-8 Sequence Middle
| (?<![\xC2-\xDF]|[\xE0-\xEF]|[\xE0-\xEF][\x80-\xBF]|[\xF0-\xF4]|[\xF0-\xF4][\x80-\xBF]|[\xF0-\xF4][\x80-\xBF]{2})[\x80-\xBF] # Overlong Sequence
| (?<=[\xE0-\xEF])[\x80-\xBF](?![\x80-\xBF]) # Short 3 byte sequence
| (?<=[\xF0-\xF4])[\x80-\xBF](?![\x80-\xBF]{2}) # Short 4 byte sequence
| (?<=[\xF0-\xF4][\x80-\xBF])[\x80-\xBF](?![\x80-\xBF]) # Short 4 byte sequence (2)
)/x';
return preg_replace_callback($regex, static function ($matches) {
assert(strlen($matches[0]) === 1);
$hex = dechex(ord($matches[0]));
return '\\x' . str_pad($hex, 2, '0', STR_PAD_LEFT);
}, $escaped);
}
}

View File

@ -0,0 +1,22 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast;
interface Node
{
public function __toString(): string;
/**
* @param mixed $value
*/
public function setAttribute(string $key, $value): void;
public function hasAttribute(string $key): bool;
/**
* @return mixed
*/
public function getAttribute(string $key);
}

View File

@ -0,0 +1,38 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast;
use function array_key_exists;
trait NodeAttributes
{
/** @var array<string, mixed> */
private $attributes = [];
/**
* @param mixed $value
*/
public function setAttribute(string $key, $value): void
{
$this->attributes[$key] = $value;
}
public function hasAttribute(string $key): bool
{
return array_key_exists($key, $this->attributes);
}
/**
* @return mixed
*/
public function getAttribute(string $key)
{
if ($this->hasAttribute($key)) {
return $this->attributes[$key];
}
return null;
}
}

View File

@ -0,0 +1,312 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast;
use LogicException;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function array_keys;
use function array_pop;
use function array_splice;
use function count;
use function get_class;
use function get_object_vars;
use function gettype;
use function is_array;
use function sprintf;
/**
* Inspired by https://github.com/nikic/PHP-Parser/tree/36a6dcd04e7b0285e8f0868f44bd4927802f7df1
*
* Copyright (c) 2011, Nikita Popov
* All rights reserved.
*/
final class NodeTraverser
{
/**
* If NodeVisitor::enterNode() returns DONT_TRAVERSE_CHILDREN, child nodes
* of the current node will not be traversed for any visitors.
*
* For subsequent visitors enterNode() will still be called on the current
* node and leaveNode() will also be invoked for the current node.
*/
public const DONT_TRAVERSE_CHILDREN = 1;
/**
* If NodeVisitor::enterNode() or NodeVisitor::leaveNode() returns
* STOP_TRAVERSAL, traversal is aborted.
*
* The afterTraverse() method will still be invoked.
*/
public const STOP_TRAVERSAL = 2;
/**
* If NodeVisitor::leaveNode() returns REMOVE_NODE for a node that occurs
* in an array, it will be removed from the array.
*
* For subsequent visitors leaveNode() will still be invoked for the
* removed node.
*/
public const REMOVE_NODE = 3;
/**
* If NodeVisitor::enterNode() returns DONT_TRAVERSE_CURRENT_AND_CHILDREN, child nodes
* of the current node will not be traversed for any visitors.
*
* For subsequent visitors enterNode() will not be called as well.
* leaveNode() will be invoked for visitors that has enterNode() method invoked.
*/
public const DONT_TRAVERSE_CURRENT_AND_CHILDREN = 4;
/** @var list<NodeVisitor> Visitors */
private $visitors = [];
/** @var bool Whether traversal should be stopped */
private $stopTraversal;
/**
* @param list<NodeVisitor> $visitors
*/
public function __construct(array $visitors)
{
$this->visitors = $visitors;
}
/**
* Traverses an array of nodes using the registered visitors.
*
* @param Node[] $nodes Array of nodes
*
* @return Node[] Traversed array of nodes
*/
public function traverse(array $nodes): array
{
$this->stopTraversal = false;
foreach ($this->visitors as $visitor) {
$return = $visitor->beforeTraverse($nodes);
if ($return === null) {
continue;
}
$nodes = $return;
}
$nodes = $this->traverseArray($nodes);
foreach ($this->visitors as $visitor) {
$return = $visitor->afterTraverse($nodes);
if ($return === null) {
continue;
}
$nodes = $return;
}
return $nodes;
}
/**
* Recursively traverse a node.
*
* @param Node $node Node to traverse.
*
* @return Node Result of traversal (may be original node or new one)
*/
private function traverseNode(Node $node): Node
{
$subNodeNames = array_keys(get_object_vars($node));
foreach ($subNodeNames as $name) {
$subNode =& $node->$name;
if (is_array($subNode)) {
$subNode = $this->traverseArray($subNode);
if ($this->stopTraversal) {
break;
}
} elseif ($subNode instanceof Node) {
$traverseChildren = true;
$breakVisitorIndex = null;
foreach ($this->visitors as $visitorIndex => $visitor) {
$return = $visitor->enterNode($subNode);
if ($return === null) {
continue;
}
if ($return instanceof Node) {
$this->ensureReplacementReasonable($subNode, $return);
$subNode = $return;
} elseif ($return === self::DONT_TRAVERSE_CHILDREN) {
$traverseChildren = false;
} elseif ($return === self::DONT_TRAVERSE_CURRENT_AND_CHILDREN) {
$traverseChildren = false;
$breakVisitorIndex = $visitorIndex;
break;
} elseif ($return === self::STOP_TRAVERSAL) {
$this->stopTraversal = true;
break 2;
} else {
throw new LogicException(
'enterNode() returned invalid value of type ' . gettype($return)
);
}
}
if ($traverseChildren) {
$subNode = $this->traverseNode($subNode);
if ($this->stopTraversal) {
break;
}
}
foreach ($this->visitors as $visitorIndex => $visitor) {
$return = $visitor->leaveNode($subNode);
if ($return !== null) {
if ($return instanceof Node) {
$this->ensureReplacementReasonable($subNode, $return);
$subNode = $return;
} elseif ($return === self::STOP_TRAVERSAL) {
$this->stopTraversal = true;
break 2;
} elseif (is_array($return)) {
throw new LogicException(
'leaveNode() may only return an array ' .
'if the parent structure is an array'
);
} else {
throw new LogicException(
'leaveNode() returned invalid value of type ' . gettype($return)
);
}
}
if ($breakVisitorIndex === $visitorIndex) {
break;
}
}
}
}
return $node;
}
/**
* Recursively traverse array (usually of nodes).
*
* @param mixed[] $nodes Array to traverse
*
* @return mixed[] Result of traversal (may be original array or changed one)
*/
private function traverseArray(array $nodes): array
{
$doNodes = [];
foreach ($nodes as $i => &$node) {
if ($node instanceof Node) {
$traverseChildren = true;
$breakVisitorIndex = null;
foreach ($this->visitors as $visitorIndex => $visitor) {
$return = $visitor->enterNode($node);
if ($return === null) {
continue;
}
if ($return instanceof Node) {
$this->ensureReplacementReasonable($node, $return);
$node = $return;
} elseif (is_array($return)) {
$doNodes[] = [$i, $return];
continue 2;
} elseif ($return === self::REMOVE_NODE) {
$doNodes[] = [$i, []];
continue 2;
} elseif ($return === self::DONT_TRAVERSE_CHILDREN) {
$traverseChildren = false;
} elseif ($return === self::DONT_TRAVERSE_CURRENT_AND_CHILDREN) {
$traverseChildren = false;
$breakVisitorIndex = $visitorIndex;
break;
} elseif ($return === self::STOP_TRAVERSAL) {
$this->stopTraversal = true;
break 2;
} else {
throw new LogicException(
'enterNode() returned invalid value of type ' . gettype($return)
);
}
}
if ($traverseChildren) {
$node = $this->traverseNode($node);
if ($this->stopTraversal) {
break;
}
}
foreach ($this->visitors as $visitorIndex => $visitor) {
$return = $visitor->leaveNode($node);
if ($return !== null) {
if ($return instanceof Node) {
$this->ensureReplacementReasonable($node, $return);
$node = $return;
} elseif (is_array($return)) {
$doNodes[] = [$i, $return];
break;
} elseif ($return === self::REMOVE_NODE) {
$doNodes[] = [$i, []];
break;
} elseif ($return === self::STOP_TRAVERSAL) {
$this->stopTraversal = true;
break 2;
} else {
throw new LogicException(
'leaveNode() returned invalid value of type ' . gettype($return)
);
}
}
if ($breakVisitorIndex === $visitorIndex) {
break;
}
}
} elseif (is_array($node)) {
throw new LogicException('Invalid node structure: Contains nested arrays');
}
}
if (count($doNodes) > 0) {
while ([$i, $replace] = array_pop($doNodes)) {
array_splice($nodes, $i, 1, $replace);
}
}
return $nodes;
}
private function ensureReplacementReasonable(Node $old, Node $new): void
{
if ($old instanceof TypeNode && !$new instanceof TypeNode) {
throw new LogicException(sprintf('Trying to replace TypeNode with %s', get_class($new)));
}
if ($old instanceof ConstExprNode && !$new instanceof ConstExprNode) {
throw new LogicException(sprintf('Trying to replace ConstExprNode with %s', get_class($new)));
}
if ($old instanceof PhpDocChildNode && !$new instanceof PhpDocChildNode) {
throw new LogicException(sprintf('Trying to replace PhpDocChildNode with %s', get_class($new)));
}
if ($old instanceof PhpDocTagValueNode && !$new instanceof PhpDocTagValueNode) {
throw new LogicException(sprintf('Trying to replace PhpDocTagValueNode with %s', get_class($new)));
}
}
}

View File

@ -0,0 +1,87 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast;
/**
* Inspired by https://github.com/nikic/PHP-Parser/tree/36a6dcd04e7b0285e8f0868f44bd4927802f7df1
*
* Copyright (c) 2011, Nikita Popov
* All rights reserved.
*/
interface NodeVisitor
{
/**
* Called once before traversal.
*
* Return value semantics:
* * null: $nodes stays as-is
* * otherwise: $nodes is set to the return value
*
* @param Node[] $nodes Array of nodes
*
* @return Node[]|null Array of nodes
*/
public function beforeTraverse(array $nodes): ?array;
/**
* Called when entering a node.
*
* Return value semantics:
* * null
* => $node stays as-is
* * array (of Nodes)
* => The return value is merged into the parent array (at the position of the $node)
* * NodeTraverser::REMOVE_NODE
* => $node is removed from the parent array
* * NodeTraverser::DONT_TRAVERSE_CHILDREN
* => Children of $node are not traversed. $node stays as-is
* * NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN
* => Further visitors for the current node are skipped, and its children are not
* traversed. $node stays as-is.
* * NodeTraverser::STOP_TRAVERSAL
* => Traversal is aborted. $node stays as-is
* * otherwise
* => $node is set to the return value
*
* @param Node $node Node
*
* @return Node|Node[]|NodeTraverser::*|null Replacement node (or special return value)
*/
public function enterNode(Node $node);
/**
* Called when leaving a node.
*
* Return value semantics:
* * null
* => $node stays as-is
* * NodeTraverser::REMOVE_NODE
* => $node is removed from the parent array
* * NodeTraverser::STOP_TRAVERSAL
* => Traversal is aborted. $node stays as-is
* * array (of Nodes)
* => The return value is merged into the parent array (at the position of the $node)
* * otherwise
* => $node is set to the return value
*
* @param Node $node Node
*
* @return Node|Node[]|NodeTraverser::REMOVE_NODE|NodeTraverser::STOP_TRAVERSAL|null Replacement node (or special return value)
*/
public function leaveNode(Node $node);
/**
* Called once after traversal.
*
* Return value semantics:
* * null: $nodes stays as-is
* * otherwise: $nodes is set to the return value
*
* @param Node[] $nodes Array of nodes
*
* @return Node[]|null Array of nodes
*/
public function afterTraverse(array $nodes): ?array;
}

View File

@ -0,0 +1,20 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\NodeVisitor;
use PHPStan\PhpDocParser\Ast\AbstractNodeVisitor;
use PHPStan\PhpDocParser\Ast\Attribute;
use PHPStan\PhpDocParser\Ast\Node;
final class CloningVisitor extends AbstractNodeVisitor
{
public function enterNode(Node $originalNode)
{
$node = clone $originalNode;
$node->setAttribute(Attribute::ORIGINAL_NODE, $originalNode);
return $node;
}
}

View File

@ -0,0 +1,50 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class AssertTagMethodValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var TypeNode */
public $type;
/** @var string */
public $parameter;
/** @var string */
public $method;
/** @var bool */
public $isNegated;
/** @var bool */
public $isEquality;
/** @var string (may be empty) */
public $description;
public function __construct(TypeNode $type, string $parameter, string $method, bool $isNegated, string $description, bool $isEquality = false)
{
$this->type = $type;
$this->parameter = $parameter;
$this->method = $method;
$this->isNegated = $isNegated;
$this->isEquality = $isEquality;
$this->description = $description;
}
public function __toString(): string
{
$isNegated = $this->isNegated ? '!' : '';
$isEquality = $this->isEquality ? '=' : '';
return trim("{$isNegated}{$isEquality}{$this->type} {$this->parameter}->{$this->method}() {$this->description}");
}
}

View File

@ -0,0 +1,50 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class AssertTagPropertyValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var TypeNode */
public $type;
/** @var string */
public $parameter;
/** @var string */
public $property;
/** @var bool */
public $isNegated;
/** @var bool */
public $isEquality;
/** @var string (may be empty) */
public $description;
public function __construct(TypeNode $type, string $parameter, string $property, bool $isNegated, string $description, bool $isEquality = false)
{
$this->type = $type;
$this->parameter = $parameter;
$this->property = $property;
$this->isNegated = $isNegated;
$this->isEquality = $isEquality;
$this->description = $description;
}
public function __toString(): string
{
$isNegated = $this->isNegated ? '!' : '';
$isEquality = $this->isEquality ? '=' : '';
return trim("{$isNegated}{$isEquality}{$this->type} {$this->parameter}->{$this->property} {$this->description}");
}
}

View File

@ -0,0 +1,46 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class AssertTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var TypeNode */
public $type;
/** @var string */
public $parameter;
/** @var bool */
public $isNegated;
/** @var bool */
public $isEquality;
/** @var string (may be empty) */
public $description;
public function __construct(TypeNode $type, string $parameter, bool $isNegated, string $description, bool $isEquality = false)
{
$this->type = $type;
$this->parameter = $parameter;
$this->isNegated = $isNegated;
$this->isEquality = $isEquality;
$this->description = $description;
}
public function __toString(): string
{
$isNegated = $this->isNegated ? '!' : '';
$isEquality = $this->isEquality ? '=' : '';
return trim("{$isNegated}{$isEquality}{$this->type} {$this->parameter} {$this->description}");
}
}

View File

@ -0,0 +1,27 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function trim;
class DeprecatedTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var string (may be empty) */
public $description;
public function __construct(string $description)
{
$this->description = $description;
}
public function __toString(): string
{
return trim($this->description);
}
}

View File

@ -0,0 +1,35 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function implode;
class DoctrineAnnotation implements Node
{
use NodeAttributes;
/** @var string */
public $name;
/** @var list<DoctrineArgument> */
public $arguments;
/**
* @param list<DoctrineArgument> $arguments
*/
public function __construct(string $name, array $arguments)
{
$this->name = $name;
$this->arguments = $arguments;
}
public function __toString(): string
{
$arguments = implode(', ', $this->arguments);
return $this->name . '(' . $arguments . ')';
}
}

View File

@ -0,0 +1,43 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
/**
* @phpstan-type ValueType = DoctrineAnnotation|IdentifierTypeNode|DoctrineArray|ConstExprNode
*/
class DoctrineArgument implements Node
{
use NodeAttributes;
/** @var IdentifierTypeNode|null */
public $key;
/** @var ValueType */
public $value;
/**
* @param ValueType $value
*/
public function __construct(?IdentifierTypeNode $key, $value)
{
$this->key = $key;
$this->value = $value;
}
public function __toString(): string
{
if ($this->key === null) {
return (string) $this->value;
}
return $this->key . '=' . $this->value;
}
}

View File

@ -0,0 +1,32 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function implode;
class DoctrineArray implements Node
{
use NodeAttributes;
/** @var list<DoctrineArrayItem> */
public $items;
/**
* @param list<DoctrineArrayItem> $items
*/
public function __construct(array $items)
{
$this->items = $items;
}
public function __toString(): string
{
$items = implode(', ', $this->items);
return '{' . $items . '}';
}
}

View File

@ -0,0 +1,47 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
/**
* @phpstan-import-type ValueType from DoctrineArgument
* @phpstan-type KeyType = ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|ConstFetchNode|null
*/
class DoctrineArrayItem implements Node
{
use NodeAttributes;
/** @var KeyType */
public $key;
/** @var ValueType */
public $value;
/**
* @param KeyType $key
* @param ValueType $value
*/
public function __construct($key, $value)
{
$this->key = $key;
$this->value = $value;
}
public function __toString(): string
{
if ($this->key === null) {
return (string) $this->value;
}
return $this->key . '=' . $this->value;
}
}

View File

@ -0,0 +1,36 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use function trim;
class DoctrineTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var DoctrineAnnotation */
public $annotation;
/** @var string (may be empty) */
public $description;
public function __construct(
DoctrineAnnotation $annotation,
string $description
)
{
$this->annotation = $annotation;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->annotation} {$this->description}");
}
}

View File

@ -0,0 +1,32 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use function trim;
class ExtendsTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var GenericTypeNode */
public $type;
/** @var string (may be empty) */
public $description;
public function __construct(GenericTypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class GenericTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var string (may be empty) */
public $value;
public function __construct(string $value)
{
$this->value = $value;
}
public function __toString(): string
{
return $this->value;
}
}

View File

@ -0,0 +1,32 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use function trim;
class ImplementsTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var GenericTypeNode */
public $type;
/** @var string (may be empty) */
public $description;
public function __construct(GenericTypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}

View File

@ -0,0 +1,53 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Parser\ParserException;
use function sprintf;
use function trigger_error;
use const E_USER_WARNING;
/**
* @property ParserException $exception
*/
class InvalidTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var string (may be empty) */
public $value;
/** @var mixed[] */
private $exceptionArgs;
public function __construct(string $value, ParserException $exception)
{
$this->value = $value;
$this->exceptionArgs = [
$exception->getCurrentTokenValue(),
$exception->getCurrentTokenType(),
$exception->getCurrentOffset(),
$exception->getExpectedTokenType(),
$exception->getExpectedTokenValue(),
$exception->getCurrentTokenLine(),
];
}
public function __get(string $name): ?ParserException
{
if ($name !== 'exception') {
trigger_error(sprintf('Undefined property: %s::$%s', self::class, $name), E_USER_WARNING);
return null;
}
return new ParserException(...$this->exceptionArgs);
}
public function __toString(): string
{
return $this->value;
}
}

View File

@ -0,0 +1,58 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function count;
use function implode;
class MethodTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var bool */
public $isStatic;
/** @var TypeNode|null */
public $returnType;
/** @var string */
public $methodName;
/** @var TemplateTagValueNode[] */
public $templateTypes;
/** @var MethodTagValueParameterNode[] */
public $parameters;
/** @var string (may be empty) */
public $description;
/**
* @param MethodTagValueParameterNode[] $parameters
* @param TemplateTagValueNode[] $templateTypes
*/
public function __construct(bool $isStatic, ?TypeNode $returnType, string $methodName, array $parameters, string $description, array $templateTypes = [])
{
$this->isStatic = $isStatic;
$this->returnType = $returnType;
$this->methodName = $methodName;
$this->parameters = $parameters;
$this->description = $description;
$this->templateTypes = $templateTypes;
}
public function __toString(): string
{
$static = $this->isStatic ? 'static ' : '';
$returnType = $this->returnType !== null ? "{$this->returnType} " : '';
$parameters = implode(', ', $this->parameters);
$description = $this->description !== '' ? " {$this->description}" : '';
$templateTypes = count($this->templateTypes) > 0 ? '<' . implode(', ', $this->templateTypes) . '>' : '';
return "{$static}{$returnType}{$this->methodName}{$templateTypes}({$parameters}){$description}";
}
}

View File

@ -0,0 +1,49 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
class MethodTagValueParameterNode implements Node
{
use NodeAttributes;
/** @var TypeNode|null */
public $type;
/** @var bool */
public $isReference;
/** @var bool */
public $isVariadic;
/** @var string */
public $parameterName;
/** @var ConstExprNode|null */
public $defaultValue;
public function __construct(?TypeNode $type, bool $isReference, bool $isVariadic, string $parameterName, ?ConstExprNode $defaultValue)
{
$this->type = $type;
$this->isReference = $isReference;
$this->isVariadic = $isVariadic;
$this->parameterName = $parameterName;
$this->defaultValue = $defaultValue;
}
public function __toString(): string
{
$type = $this->type !== null ? "{$this->type} " : '';
$isReference = $this->isReference ? '&' : '';
$isVariadic = $this->isVariadic ? '...' : '';
$default = $this->defaultValue !== null ? " = {$this->defaultValue}" : '';
return "{$type}{$isReference}{$isVariadic}{$this->parameterName}{$default}";
}
}

View File

@ -0,0 +1,32 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class MixinTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var TypeNode */
public $type;
/** @var string (may be empty) */
public $description;
public function __construct(TypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}

View File

@ -0,0 +1,35 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class ParamClosureThisTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var TypeNode */
public $type;
/** @var string */
public $parameterName;
/** @var string (may be empty) */
public $description;
public function __construct(TypeNode $type, string $parameterName, string $description)
{
$this->type = $type;
$this->parameterName = $parameterName;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->parameterName} {$this->description}");
}
}

View File

@ -0,0 +1,30 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function trim;
class ParamImmediatelyInvokedCallableTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var string */
public $parameterName;
/** @var string (may be empty) */
public $description;
public function __construct(string $parameterName, string $description)
{
$this->parameterName = $parameterName;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->parameterName} {$this->description}");
}
}

View File

@ -0,0 +1,30 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function trim;
class ParamLaterInvokedCallableTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var string */
public $parameterName;
/** @var string (may be empty) */
public $description;
public function __construct(string $parameterName, string $description)
{
$this->parameterName = $parameterName;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->parameterName} {$this->description}");
}
}

View File

@ -0,0 +1,35 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class ParamOutTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var TypeNode */
public $type;
/** @var string */
public $parameterName;
/** @var string (may be empty) */
public $description;
public function __construct(TypeNode $type, string $parameterName, string $description)
{
$this->type = $type;
$this->parameterName = $parameterName;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->parameterName} {$this->description}");
}
}

View File

@ -0,0 +1,46 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class ParamTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var TypeNode */
public $type;
/** @var bool */
public $isReference;
/** @var bool */
public $isVariadic;
/** @var string */
public $parameterName;
/** @var string (may be empty) */
public $description;
public function __construct(TypeNode $type, bool $isVariadic, string $parameterName, string $description, bool $isReference = false)
{
$this->type = $type;
$this->isReference = $isReference;
$this->isVariadic = $isVariadic;
$this->parameterName = $parameterName;
$this->description = $description;
}
public function __toString(): string
{
$reference = $this->isReference ? '&' : '';
$variadic = $this->isVariadic ? '...' : '';
return trim("{$this->type} {$reference}{$variadic}{$this->parameterName} {$this->description}");
}
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\Node;
interface PhpDocChildNode extends Node
{
}

View File

@ -0,0 +1,438 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function array_column;
use function array_filter;
use function array_map;
use function implode;
class PhpDocNode implements Node
{
use NodeAttributes;
/** @var PhpDocChildNode[] */
public $children;
/**
* @param PhpDocChildNode[] $children
*/
public function __construct(array $children)
{
$this->children = $children;
}
/**
* @return PhpDocTagNode[]
*/
public function getTags(): array
{
return array_filter($this->children, static function (PhpDocChildNode $child): bool {
return $child instanceof PhpDocTagNode;
});
}
/**
* @return PhpDocTagNode[]
*/
public function getTagsByName(string $tagName): array
{
return array_filter($this->getTags(), static function (PhpDocTagNode $tag) use ($tagName): bool {
return $tag->name === $tagName;
});
}
/**
* @return VarTagValueNode[]
*/
public function getVarTagValues(string $tagName = '@var'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof VarTagValueNode;
}
);
}
/**
* @return ParamTagValueNode[]
*/
public function getParamTagValues(string $tagName = '@param'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof ParamTagValueNode;
}
);
}
/**
* @return TypelessParamTagValueNode[]
*/
public function getTypelessParamTagValues(string $tagName = '@param'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof TypelessParamTagValueNode;
}
);
}
/**
* @return ParamImmediatelyInvokedCallableTagValueNode[]
*/
public function getParamImmediatelyInvokedCallableTagValues(string $tagName = '@param-immediately-invoked-callable'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof ParamImmediatelyInvokedCallableTagValueNode;
}
);
}
/**
* @return ParamLaterInvokedCallableTagValueNode[]
*/
public function getParamLaterInvokedCallableTagValues(string $tagName = '@param-later-invoked-callable'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof ParamLaterInvokedCallableTagValueNode;
}
);
}
/**
* @return ParamClosureThisTagValueNode[]
*/
public function getParamClosureThisTagValues(string $tagName = '@param-closure-this'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof ParamClosureThisTagValueNode;
}
);
}
/**
* @return TemplateTagValueNode[]
*/
public function getTemplateTagValues(string $tagName = '@template'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof TemplateTagValueNode;
}
);
}
/**
* @return ExtendsTagValueNode[]
*/
public function getExtendsTagValues(string $tagName = '@extends'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof ExtendsTagValueNode;
}
);
}
/**
* @return ImplementsTagValueNode[]
*/
public function getImplementsTagValues(string $tagName = '@implements'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof ImplementsTagValueNode;
}
);
}
/**
* @return UsesTagValueNode[]
*/
public function getUsesTagValues(string $tagName = '@use'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof UsesTagValueNode;
}
);
}
/**
* @return ReturnTagValueNode[]
*/
public function getReturnTagValues(string $tagName = '@return'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof ReturnTagValueNode;
}
);
}
/**
* @return ThrowsTagValueNode[]
*/
public function getThrowsTagValues(string $tagName = '@throws'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof ThrowsTagValueNode;
}
);
}
/**
* @return MixinTagValueNode[]
*/
public function getMixinTagValues(string $tagName = '@mixin'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof MixinTagValueNode;
}
);
}
/**
* @return RequireExtendsTagValueNode[]
*/
public function getRequireExtendsTagValues(string $tagName = '@phpstan-require-extends'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof RequireExtendsTagValueNode;
}
);
}
/**
* @return RequireImplementsTagValueNode[]
*/
public function getRequireImplementsTagValues(string $tagName = '@phpstan-require-implements'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof RequireImplementsTagValueNode;
}
);
}
/**
* @return DeprecatedTagValueNode[]
*/
public function getDeprecatedTagValues(): array
{
return array_filter(
array_column($this->getTagsByName('@deprecated'), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof DeprecatedTagValueNode;
}
);
}
/**
* @return PropertyTagValueNode[]
*/
public function getPropertyTagValues(string $tagName = '@property'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof PropertyTagValueNode;
}
);
}
/**
* @return PropertyTagValueNode[]
*/
public function getPropertyReadTagValues(string $tagName = '@property-read'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof PropertyTagValueNode;
}
);
}
/**
* @return PropertyTagValueNode[]
*/
public function getPropertyWriteTagValues(string $tagName = '@property-write'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof PropertyTagValueNode;
}
);
}
/**
* @return MethodTagValueNode[]
*/
public function getMethodTagValues(string $tagName = '@method'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof MethodTagValueNode;
}
);
}
/**
* @return TypeAliasTagValueNode[]
*/
public function getTypeAliasTagValues(string $tagName = '@phpstan-type'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof TypeAliasTagValueNode;
}
);
}
/**
* @return TypeAliasImportTagValueNode[]
*/
public function getTypeAliasImportTagValues(string $tagName = '@phpstan-import-type'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof TypeAliasImportTagValueNode;
}
);
}
/**
* @return AssertTagValueNode[]
*/
public function getAssertTagValues(string $tagName = '@phpstan-assert'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof AssertTagValueNode;
}
);
}
/**
* @return AssertTagPropertyValueNode[]
*/
public function getAssertPropertyTagValues(string $tagName = '@phpstan-assert'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof AssertTagPropertyValueNode;
}
);
}
/**
* @return AssertTagMethodValueNode[]
*/
public function getAssertMethodTagValues(string $tagName = '@phpstan-assert'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof AssertTagMethodValueNode;
}
);
}
/**
* @return SelfOutTagValueNode[]
*/
public function getSelfOutTypeTagValues(string $tagName = '@phpstan-this-out'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof SelfOutTagValueNode;
}
);
}
/**
* @return ParamOutTagValueNode[]
*/
public function getParamOutTypeTagValues(string $tagName = '@param-out'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static function (PhpDocTagValueNode $value): bool {
return $value instanceof ParamOutTagValueNode;
}
);
}
public function __toString(): string
{
$children = array_map(
static function (PhpDocChildNode $child): string {
$s = (string) $child;
return $s === '' ? '' : ' ' . $s;
},
$this->children
);
return "/**\n *" . implode("\n *", $children) . "\n */";
}
}

View File

@ -0,0 +1,36 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineTagValueNode;
use function trim;
class PhpDocTagNode implements PhpDocChildNode
{
use NodeAttributes;
/** @var string */
public $name;
/** @var PhpDocTagValueNode */
public $value;
public function __construct(string $name, PhpDocTagValueNode $value)
{
$this->name = $name;
$this->value = $value;
}
public function __toString(): string
{
if ($this->value instanceof DoctrineTagValueNode) {
return (string) $this->value;
}
return trim("{$this->name} {$this->value}");
}
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\Node;
interface PhpDocTagValueNode extends Node
{
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class PhpDocTextNode implements PhpDocChildNode
{
use NodeAttributes;
/** @var string */
public $text;
public function __construct(string $text)
{
$this->text = $text;
}
public function __toString(): string
{
return $this->text;
}
}

View File

@ -0,0 +1,36 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class PropertyTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var TypeNode */
public $type;
/** @var string */
public $propertyName;
/** @var string (may be empty) */
public $description;
public function __construct(TypeNode $type, string $propertyName, string $description)
{
$this->type = $type;
$this->propertyName = $propertyName;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->propertyName} {$this->description}");
}
}

View File

@ -0,0 +1,32 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class RequireExtendsTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var TypeNode */
public $type;
/** @var string (may be empty) */
public $description;
public function __construct(TypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}

View File

@ -0,0 +1,32 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class RequireImplementsTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var TypeNode */
public $type;
/** @var string (may be empty) */
public $description;
public function __construct(TypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}

View File

@ -0,0 +1,32 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class ReturnTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var TypeNode */
public $type;
/** @var string (may be empty) */
public $description;
public function __construct(TypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}

View File

@ -0,0 +1,32 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class SelfOutTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var TypeNode */
public $type;
/** @var string (may be empty) */
public $description;
public function __construct(TypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim($this->type . ' ' . $this->description);
}
}

View File

@ -0,0 +1,50 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class TemplateTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var non-empty-string */
public $name;
/** @var TypeNode|null */
public $bound;
/** @var TypeNode|null */
public $lowerBound;
/** @var TypeNode|null */
public $default;
/** @var string (may be empty) */
public $description;
/**
* @param non-empty-string $name
*/
public function __construct(string $name, ?TypeNode $bound, string $description, ?TypeNode $default = null, ?TypeNode $lowerBound = null)
{
$this->name = $name;
$this->bound = $bound;
$this->lowerBound = $lowerBound;
$this->default = $default;
$this->description = $description;
}
public function __toString(): string
{
$upperBound = $this->bound !== null ? " of {$this->bound}" : '';
$lowerBound = $this->lowerBound !== null ? " super {$this->lowerBound}" : '';
$default = $this->default !== null ? " = {$this->default}" : '';
return trim("{$this->name}{$upperBound}{$lowerBound}{$default} {$this->description}");
}
}

View File

@ -0,0 +1,32 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class ThrowsTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var TypeNode */
public $type;
/** @var string (may be empty) */
public $description;
public function __construct(TypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}

View File

@ -0,0 +1,38 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use function trim;
class TypeAliasImportTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var string */
public $importedAlias;
/** @var IdentifierTypeNode */
public $importedFrom;
/** @var string|null */
public $importedAs;
public function __construct(string $importedAlias, IdentifierTypeNode $importedFrom, ?string $importedAs)
{
$this->importedAlias = $importedAlias;
$this->importedFrom = $importedFrom;
$this->importedAs = $importedAs;
}
public function __toString(): string
{
return trim(
"{$this->importedAlias} from {$this->importedFrom}"
. ($this->importedAs !== null ? " as {$this->importedAs}" : '')
);
}
}

View File

@ -0,0 +1,32 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class TypeAliasTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var string */
public $alias;
/** @var TypeNode */
public $type;
public function __construct(string $alias, TypeNode $type)
{
$this->alias = $alias;
$this->type = $type;
}
public function __toString(): string
{
return trim("{$this->alias} {$this->type}");
}
}

View File

@ -0,0 +1,41 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function trim;
class TypelessParamTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var bool */
public $isReference;
/** @var bool */
public $isVariadic;
/** @var string */
public $parameterName;
/** @var string (may be empty) */
public $description;
public function __construct(bool $isVariadic, string $parameterName, string $description, bool $isReference = false)
{
$this->isReference = $isReference;
$this->isVariadic = $isVariadic;
$this->parameterName = $parameterName;
$this->description = $description;
}
public function __toString(): string
{
$reference = $this->isReference ? '&' : '';
$variadic = $this->isVariadic ? '...' : '';
return trim("{$reference}{$variadic}{$this->parameterName} {$this->description}");
}
}

View File

@ -0,0 +1,32 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use function trim;
class UsesTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var GenericTypeNode */
public $type;
/** @var string (may be empty) */
public $description;
public function __construct(GenericTypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}

View File

@ -0,0 +1,36 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class VarTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var TypeNode */
public $type;
/** @var string (may be empty) */
public $variableName;
/** @var string (may be empty) */
public $description;
public function __construct(TypeNode $type, string $variableName, string $description)
{
$this->type = $type;
$this->variableName = $variableName;
$this->description = $description;
}
public function __toString(): string
{
return trim("$this->type " . trim("{$this->variableName} {$this->description}"));
}
}

View File

@ -0,0 +1,49 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;
class ArrayShapeItemNode implements TypeNode
{
use NodeAttributes;
/** @var ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|null */
public $keyName;
/** @var bool */
public $optional;
/** @var TypeNode */
public $valueType;
/**
* @param ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|null $keyName
*/
public function __construct($keyName, bool $optional, TypeNode $valueType)
{
$this->keyName = $keyName;
$this->optional = $optional;
$this->valueType = $valueType;
}
public function __toString(): string
{
if ($this->keyName !== null) {
return sprintf(
'%s%s: %s',
(string) $this->keyName,
$this->optional ? '?' : '',
(string) $this->valueType
);
}
return (string) $this->valueType;
}
}

View File

@ -0,0 +1,57 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function implode;
class ArrayShapeNode implements TypeNode
{
public const KIND_ARRAY = 'array';
public const KIND_LIST = 'list';
use NodeAttributes;
/** @var ArrayShapeItemNode[] */
public $items;
/** @var bool */
public $sealed;
/** @var self::KIND_* */
public $kind;
/** @var ArrayShapeUnsealedTypeNode|null */
public $unsealedType;
/**
* @param ArrayShapeItemNode[] $items
* @param self::KIND_* $kind
*/
public function __construct(
array $items,
bool $sealed = true,
string $kind = self::KIND_ARRAY,
?ArrayShapeUnsealedTypeNode $unsealedType = null
)
{
$this->items = $items;
$this->sealed = $sealed;
$this->kind = $kind;
$this->unsealedType = $unsealedType;
}
public function __toString(): string
{
$items = $this->items;
if (! $this->sealed) {
$items[] = '...' . $this->unsealedType;
}
return $this->kind . '{' . implode(', ', $items) . '}';
}
}

View File

@ -0,0 +1,34 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;
class ArrayShapeUnsealedTypeNode implements Node
{
use NodeAttributes;
/** @var TypeNode */
public $valueType;
/** @var TypeNode|null */
public $keyType;
public function __construct(TypeNode $valueType, ?TypeNode $keyType)
{
$this->valueType = $valueType;
$this->keyType = $keyType;
}
public function __toString(): string
{
if ($this->keyType !== null) {
return sprintf('<%s, %s>', $this->keyType, $this->valueType);
}
return sprintf('<%s>', $this->valueType);
}
}

View File

@ -0,0 +1,34 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ArrayTypeNode implements TypeNode
{
use NodeAttributes;
/** @var TypeNode */
public $type;
public function __construct(TypeNode $type)
{
$this->type = $type;
}
public function __toString(): string
{
if (
$this->type instanceof CallableTypeNode
|| $this->type instanceof ConstTypeNode
|| $this->type instanceof NullableTypeNode
) {
return '(' . $this->type . ')[]';
}
return $this->type . '[]';
}
}

View File

@ -0,0 +1,52 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use function implode;
class CallableTypeNode implements TypeNode
{
use NodeAttributes;
/** @var IdentifierTypeNode */
public $identifier;
/** @var TemplateTagValueNode[] */
public $templateTypes;
/** @var CallableTypeParameterNode[] */
public $parameters;
/** @var TypeNode */
public $returnType;
/**
* @param CallableTypeParameterNode[] $parameters
* @param TemplateTagValueNode[] $templateTypes
*/
public function __construct(IdentifierTypeNode $identifier, array $parameters, TypeNode $returnType, array $templateTypes = [])
{
$this->identifier = $identifier;
$this->parameters = $parameters;
$this->returnType = $returnType;
$this->templateTypes = $templateTypes;
}
public function __toString(): string
{
$returnType = $this->returnType;
if ($returnType instanceof self) {
$returnType = "({$returnType})";
}
$template = $this->templateTypes !== []
? '<' . implode(', ', $this->templateTypes) . '>'
: '';
$parameters = implode(', ', $this->parameters);
return "{$this->identifier}{$template}({$parameters}): {$returnType}";
}
}

View File

@ -0,0 +1,48 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function trim;
class CallableTypeParameterNode implements Node
{
use NodeAttributes;
/** @var TypeNode */
public $type;
/** @var bool */
public $isReference;
/** @var bool */
public $isVariadic;
/** @var string (may be empty) */
public $parameterName;
/** @var bool */
public $isOptional;
public function __construct(TypeNode $type, bool $isReference, bool $isVariadic, string $parameterName, bool $isOptional)
{
$this->type = $type;
$this->isReference = $isReference;
$this->isVariadic = $isVariadic;
$this->parameterName = $parameterName;
$this->isOptional = $isOptional;
}
public function __toString(): string
{
$type = "{$this->type} ";
$isReference = $this->isReference ? '&' : '';
$isVariadic = $this->isVariadic ? '...' : '';
$isOptional = $this->isOptional ? '=' : '';
return trim("{$type}{$isReference}{$isVariadic}{$this->parameterName}") . $isOptional;
}
}

View File

@ -0,0 +1,49 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;
class ConditionalTypeForParameterNode implements TypeNode
{
use NodeAttributes;
/** @var string */
public $parameterName;
/** @var TypeNode */
public $targetType;
/** @var TypeNode */
public $if;
/** @var TypeNode */
public $else;
/** @var bool */
public $negated;
public function __construct(string $parameterName, TypeNode $targetType, TypeNode $if, TypeNode $else, bool $negated)
{
$this->parameterName = $parameterName;
$this->targetType = $targetType;
$this->if = $if;
$this->else = $else;
$this->negated = $negated;
}
public function __toString(): string
{
return sprintf(
'(%s %s %s ? %s : %s)',
$this->parameterName,
$this->negated ? 'is not' : 'is',
$this->targetType,
$this->if,
$this->else
);
}
}

View File

@ -0,0 +1,49 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;
class ConditionalTypeNode implements TypeNode
{
use NodeAttributes;
/** @var TypeNode */
public $subjectType;
/** @var TypeNode */
public $targetType;
/** @var TypeNode */
public $if;
/** @var TypeNode */
public $else;
/** @var bool */
public $negated;
public function __construct(TypeNode $subjectType, TypeNode $targetType, TypeNode $if, TypeNode $else, bool $negated)
{
$this->subjectType = $subjectType;
$this->targetType = $targetType;
$this->if = $if;
$this->else = $else;
$this->negated = $negated;
}
public function __toString(): string
{
return sprintf(
'(%s %s %s ? %s : %s)',
$this->subjectType,
$this->negated ? 'is not' : 'is',
$this->targetType,
$this->if,
$this->else
);
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ConstTypeNode implements TypeNode
{
use NodeAttributes;
/** @var ConstExprNode */
public $constExpr;
public function __construct(ConstExprNode $constExpr)
{
$this->constExpr = $constExpr;
}
public function __toString(): string
{
return $this->constExpr->__toString();
}
}

View File

@ -0,0 +1,58 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function implode;
use function sprintf;
class GenericTypeNode implements TypeNode
{
public const VARIANCE_INVARIANT = 'invariant';
public const VARIANCE_COVARIANT = 'covariant';
public const VARIANCE_CONTRAVARIANT = 'contravariant';
public const VARIANCE_BIVARIANT = 'bivariant';
use NodeAttributes;
/** @var IdentifierTypeNode */
public $type;
/** @var TypeNode[] */
public $genericTypes;
/** @var (self::VARIANCE_*)[] */
public $variances;
/**
* @param TypeNode[] $genericTypes
* @param (self::VARIANCE_*)[] $variances
*/
public function __construct(IdentifierTypeNode $type, array $genericTypes, array $variances = [])
{
$this->type = $type;
$this->genericTypes = $genericTypes;
$this->variances = $variances;
}
public function __toString(): string
{
$genericTypes = [];
foreach ($this->genericTypes as $index => $type) {
$variance = $this->variances[$index] ?? self::VARIANCE_INVARIANT;
if ($variance === self::VARIANCE_INVARIANT) {
$genericTypes[] = (string) $type;
} elseif ($variance === self::VARIANCE_BIVARIANT) {
$genericTypes[] = '*';
} else {
$genericTypes[] = sprintf('%s %s', $variance, $type);
}
}
return $this->type . '<' . implode(', ', $genericTypes) . '>';
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class IdentifierTypeNode implements TypeNode
{
use NodeAttributes;
/** @var string */
public $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function __toString(): string
{
return $this->name;
}
}

View File

@ -0,0 +1,37 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function array_map;
use function implode;
class IntersectionTypeNode implements TypeNode
{
use NodeAttributes;
/** @var TypeNode[] */
public $types;
/**
* @param TypeNode[] $types
*/
public function __construct(array $types)
{
$this->types = $types;
}
public function __toString(): string
{
return '(' . implode(' & ', array_map(static function (TypeNode $type): string {
if ($type instanceof NullableTypeNode) {
return '(' . $type . ')';
}
return (string) $type;
}, $this->types)) . ')';
}
}

View File

@ -0,0 +1,38 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Parser\ParserException;
class InvalidTypeNode implements TypeNode
{
use NodeAttributes;
/** @var mixed[] */
private $exceptionArgs;
public function __construct(ParserException $exception)
{
$this->exceptionArgs = [
$exception->getCurrentTokenValue(),
$exception->getCurrentTokenType(),
$exception->getCurrentOffset(),
$exception->getExpectedTokenType(),
$exception->getExpectedTokenValue(),
$exception->getCurrentTokenLine(),
];
}
public function getException(): ParserException
{
return new ParserException(...$this->exceptionArgs);
}
public function __toString(): string
{
return '*Invalid type*';
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class NullableTypeNode implements TypeNode
{
use NodeAttributes;
/** @var TypeNode */
public $type;
public function __construct(TypeNode $type)
{
$this->type = $type;
}
public function __toString(): string
{
return '?' . $this->type;
}
}

View File

@ -0,0 +1,48 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;
class ObjectShapeItemNode implements TypeNode
{
use NodeAttributes;
/** @var ConstExprStringNode|IdentifierTypeNode */
public $keyName;
/** @var bool */
public $optional;
/** @var TypeNode */
public $valueType;
/**
* @param ConstExprStringNode|IdentifierTypeNode $keyName
*/
public function __construct($keyName, bool $optional, TypeNode $valueType)
{
$this->keyName = $keyName;
$this->optional = $optional;
$this->valueType = $valueType;
}
public function __toString(): string
{
if ($this->keyName !== null) {
return sprintf(
'%s%s: %s',
(string) $this->keyName,
$this->optional ? '?' : '',
(string) $this->valueType
);
}
return (string) $this->valueType;
}
}

View File

@ -0,0 +1,31 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function implode;
class ObjectShapeNode implements TypeNode
{
use NodeAttributes;
/** @var ObjectShapeItemNode[] */
public $items;
/**
* @param ObjectShapeItemNode[] $items
*/
public function __construct(array $items)
{
$this->items = $items;
}
public function __toString(): string
{
$items = $this->items;
return 'object{' . implode(', ', $items) . '}';
}
}

View File

@ -0,0 +1,36 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class OffsetAccessTypeNode implements TypeNode
{
use NodeAttributes;
/** @var TypeNode */
public $type;
/** @var TypeNode */
public $offset;
public function __construct(TypeNode $type, TypeNode $offset)
{
$this->type = $type;
$this->offset = $offset;
}
public function __toString(): string
{
if (
$this->type instanceof CallableTypeNode
|| $this->type instanceof NullableTypeNode
) {
return '(' . $this->type . ')[' . $this->offset . ']';
}
return $this->type . '[' . $this->offset . ']';
}
}

View File

@ -0,0 +1,17 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ThisTypeNode implements TypeNode
{
use NodeAttributes;
public function __toString(): string
{
return '$this';
}
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\Node;
interface TypeNode extends Node
{
}

View File

@ -0,0 +1,37 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function array_map;
use function implode;
class UnionTypeNode implements TypeNode
{
use NodeAttributes;
/** @var TypeNode[] */
public $types;
/**
* @param TypeNode[] $types
*/
public function __construct(array $types)
{
$this->types = $types;
}
public function __toString(): string
{
return '(' . implode(' | ', array_map(static function (TypeNode $type): string {
if ($type instanceof NullableTypeNode) {
return '(' . $type . ')';
}
return (string) $type;
}, $this->types)) . ')';
}
}

View File

@ -0,0 +1,198 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Lexer;
use function implode;
use function preg_match_all;
use const PREG_SET_ORDER;
/**
* Implementation based on Nette Tokenizer (New BSD License; https://github.com/nette/tokenizer)
*/
class Lexer
{
public const TOKEN_REFERENCE = 0;
public const TOKEN_UNION = 1;
public const TOKEN_INTERSECTION = 2;
public const TOKEN_NULLABLE = 3;
public const TOKEN_OPEN_PARENTHESES = 4;
public const TOKEN_CLOSE_PARENTHESES = 5;
public const TOKEN_OPEN_ANGLE_BRACKET = 6;
public const TOKEN_CLOSE_ANGLE_BRACKET = 7;
public const TOKEN_OPEN_SQUARE_BRACKET = 8;
public const TOKEN_CLOSE_SQUARE_BRACKET = 9;
public const TOKEN_COMMA = 10;
public const TOKEN_VARIADIC = 11;
public const TOKEN_DOUBLE_COLON = 12;
public const TOKEN_DOUBLE_ARROW = 13;
public const TOKEN_EQUAL = 14;
public const TOKEN_OPEN_PHPDOC = 15;
public const TOKEN_CLOSE_PHPDOC = 16;
public const TOKEN_PHPDOC_TAG = 17;
public const TOKEN_DOCTRINE_TAG = 18;
public const TOKEN_FLOAT = 19;
public const TOKEN_INTEGER = 20;
public const TOKEN_SINGLE_QUOTED_STRING = 21;
public const TOKEN_DOUBLE_QUOTED_STRING = 22;
public const TOKEN_DOCTRINE_ANNOTATION_STRING = 23;
public const TOKEN_IDENTIFIER = 24;
public const TOKEN_THIS_VARIABLE = 25;
public const TOKEN_VARIABLE = 26;
public const TOKEN_HORIZONTAL_WS = 27;
public const TOKEN_PHPDOC_EOL = 28;
public const TOKEN_OTHER = 29;
public const TOKEN_END = 30;
public const TOKEN_COLON = 31;
public const TOKEN_WILDCARD = 32;
public const TOKEN_OPEN_CURLY_BRACKET = 33;
public const TOKEN_CLOSE_CURLY_BRACKET = 34;
public const TOKEN_NEGATED = 35;
public const TOKEN_ARROW = 36;
public const TOKEN_LABELS = [
self::TOKEN_REFERENCE => '\'&\'',
self::TOKEN_UNION => '\'|\'',
self::TOKEN_INTERSECTION => '\'&\'',
self::TOKEN_NULLABLE => '\'?\'',
self::TOKEN_NEGATED => '\'!\'',
self::TOKEN_OPEN_PARENTHESES => '\'(\'',
self::TOKEN_CLOSE_PARENTHESES => '\')\'',
self::TOKEN_OPEN_ANGLE_BRACKET => '\'<\'',
self::TOKEN_CLOSE_ANGLE_BRACKET => '\'>\'',
self::TOKEN_OPEN_SQUARE_BRACKET => '\'[\'',
self::TOKEN_CLOSE_SQUARE_BRACKET => '\']\'',
self::TOKEN_OPEN_CURLY_BRACKET => '\'{\'',
self::TOKEN_CLOSE_CURLY_BRACKET => '\'}\'',
self::TOKEN_COMMA => '\',\'',
self::TOKEN_COLON => '\':\'',
self::TOKEN_VARIADIC => '\'...\'',
self::TOKEN_DOUBLE_COLON => '\'::\'',
self::TOKEN_DOUBLE_ARROW => '\'=>\'',
self::TOKEN_ARROW => '\'->\'',
self::TOKEN_EQUAL => '\'=\'',
self::TOKEN_OPEN_PHPDOC => '\'/**\'',
self::TOKEN_CLOSE_PHPDOC => '\'*/\'',
self::TOKEN_PHPDOC_TAG => 'TOKEN_PHPDOC_TAG',
self::TOKEN_DOCTRINE_TAG => 'TOKEN_DOCTRINE_TAG',
self::TOKEN_PHPDOC_EOL => 'TOKEN_PHPDOC_EOL',
self::TOKEN_FLOAT => 'TOKEN_FLOAT',
self::TOKEN_INTEGER => 'TOKEN_INTEGER',
self::TOKEN_SINGLE_QUOTED_STRING => 'TOKEN_SINGLE_QUOTED_STRING',
self::TOKEN_DOUBLE_QUOTED_STRING => 'TOKEN_DOUBLE_QUOTED_STRING',
self::TOKEN_DOCTRINE_ANNOTATION_STRING => 'TOKEN_DOCTRINE_ANNOTATION_STRING',
self::TOKEN_IDENTIFIER => 'type',
self::TOKEN_THIS_VARIABLE => '\'$this\'',
self::TOKEN_VARIABLE => 'variable',
self::TOKEN_HORIZONTAL_WS => 'TOKEN_HORIZONTAL_WS',
self::TOKEN_OTHER => 'TOKEN_OTHER',
self::TOKEN_END => 'TOKEN_END',
self::TOKEN_WILDCARD => '*',
];
public const VALUE_OFFSET = 0;
public const TYPE_OFFSET = 1;
public const LINE_OFFSET = 2;
/** @var bool */
private $parseDoctrineAnnotations;
/** @var string|null */
private $regexp;
public function __construct(bool $parseDoctrineAnnotations = false)
{
$this->parseDoctrineAnnotations = $parseDoctrineAnnotations;
}
/**
* @return list<array{string, int, int}>
*/
public function tokenize(string $s): array
{
if ($this->regexp === null) {
$this->regexp = $this->generateRegexp();
}
preg_match_all($this->regexp, $s, $matches, PREG_SET_ORDER);
$tokens = [];
$line = 1;
foreach ($matches as $match) {
$type = (int) $match['MARK'];
$tokens[] = [$match[0], $type, $line];
if ($type !== self::TOKEN_PHPDOC_EOL) {
continue;
}
$line++;
}
$tokens[] = ['', self::TOKEN_END, $line];
return $tokens;
}
private function generateRegexp(): string
{
$patterns = [
self::TOKEN_HORIZONTAL_WS => '[\\x09\\x20]++',
self::TOKEN_IDENTIFIER => '(?:[\\\\]?+[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF-]*+)++',
self::TOKEN_THIS_VARIABLE => '\\$this(?![0-9a-z_\\x80-\\xFF])',
self::TOKEN_VARIABLE => '\\$[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF]*+',
// '&' followed by TOKEN_VARIADIC, TOKEN_VARIABLE, TOKEN_EQUAL, TOKEN_EQUAL or TOKEN_CLOSE_PARENTHESES
self::TOKEN_REFERENCE => '&(?=\\s*+(?:[.,=)]|(?:\\$(?!this(?![0-9a-z_\\x80-\\xFF])))))',
self::TOKEN_UNION => '\\|',
self::TOKEN_INTERSECTION => '&',
self::TOKEN_NULLABLE => '\\?',
self::TOKEN_NEGATED => '!',
self::TOKEN_OPEN_PARENTHESES => '\\(',
self::TOKEN_CLOSE_PARENTHESES => '\\)',
self::TOKEN_OPEN_ANGLE_BRACKET => '<',
self::TOKEN_CLOSE_ANGLE_BRACKET => '>',
self::TOKEN_OPEN_SQUARE_BRACKET => '\\[',
self::TOKEN_CLOSE_SQUARE_BRACKET => '\\]',
self::TOKEN_OPEN_CURLY_BRACKET => '\\{',
self::TOKEN_CLOSE_CURLY_BRACKET => '\\}',
self::TOKEN_COMMA => ',',
self::TOKEN_VARIADIC => '\\.\\.\\.',
self::TOKEN_DOUBLE_COLON => '::',
self::TOKEN_DOUBLE_ARROW => '=>',
self::TOKEN_ARROW => '->',
self::TOKEN_EQUAL => '=',
self::TOKEN_COLON => ':',
self::TOKEN_OPEN_PHPDOC => '/\\*\\*(?=\\s)\\x20?+',
self::TOKEN_CLOSE_PHPDOC => '\\*/',
self::TOKEN_PHPDOC_TAG => '@(?:[a-z][a-z0-9-\\\\]+:)?[a-z][a-z0-9-\\\\]*+',
self::TOKEN_PHPDOC_EOL => '\\r?+\\n[\\x09\\x20]*+(?:\\*(?!/)\\x20?+)?',
self::TOKEN_FLOAT => '[+\-]?(?:(?:[0-9]++(_[0-9]++)*\\.[0-9]*+(_[0-9]++)*(?:e[+\-]?[0-9]++(_[0-9]++)*)?)|(?:[0-9]*+(_[0-9]++)*\\.[0-9]++(_[0-9]++)*(?:e[+\-]?[0-9]++(_[0-9]++)*)?)|(?:[0-9]++(_[0-9]++)*e[+\-]?[0-9]++(_[0-9]++)*))',
self::TOKEN_INTEGER => '[+\-]?(?:(?:0b[0-1]++(_[0-1]++)*)|(?:0o[0-7]++(_[0-7]++)*)|(?:0x[0-9a-f]++(_[0-9a-f]++)*)|(?:[0-9]++(_[0-9]++)*))',
self::TOKEN_SINGLE_QUOTED_STRING => '\'(?:\\\\[^\\r\\n]|[^\'\\r\\n\\\\])*+\'',
self::TOKEN_DOUBLE_QUOTED_STRING => '"(?:\\\\[^\\r\\n]|[^"\\r\\n\\\\])*+"',
self::TOKEN_WILDCARD => '\\*',
];
if ($this->parseDoctrineAnnotations) {
$patterns[self::TOKEN_DOCTRINE_TAG] = '@[a-z_\\\\][a-z0-9_\:\\\\]*[a-z_][a-z0-9_]*';
$patterns[self::TOKEN_DOCTRINE_ANNOTATION_STRING] = '"(?:""|[^"])*+"';
}
// anything but TOKEN_CLOSE_PHPDOC or TOKEN_HORIZONTAL_WS or TOKEN_EOL
$patterns[self::TOKEN_OTHER] = '(?:(?!\\*/)[^\\s])++';
foreach ($patterns as $type => &$pattern) {
$pattern = '(?:' . $pattern . ')(*MARK:' . $type . ')';
}
return '~' . implode('|', $patterns) . '~Asi';
}
}

View File

@ -0,0 +1,333 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Parser;
use PHPStan\PhpDocParser\Ast;
use PHPStan\PhpDocParser\Lexer\Lexer;
use function str_replace;
use function strtolower;
use function substr;
class ConstExprParser
{
/** @var bool */
private $unescapeStrings;
/** @var bool */
private $quoteAwareConstExprString;
/** @var bool */
private $useLinesAttributes;
/** @var bool */
private $useIndexAttributes;
/** @var bool */
private $parseDoctrineStrings;
/**
* @param array{lines?: bool, indexes?: bool} $usedAttributes
*/
public function __construct(
bool $unescapeStrings = false,
bool $quoteAwareConstExprString = false,
array $usedAttributes = []
)
{
$this->unescapeStrings = $unescapeStrings;
$this->quoteAwareConstExprString = $quoteAwareConstExprString;
$this->useLinesAttributes = $usedAttributes['lines'] ?? false;
$this->useIndexAttributes = $usedAttributes['indexes'] ?? false;
$this->parseDoctrineStrings = false;
}
/**
* @internal
*/
public function toDoctrine(): self
{
$self = new self(
$this->unescapeStrings,
$this->quoteAwareConstExprString,
[
'lines' => $this->useLinesAttributes,
'indexes' => $this->useIndexAttributes,
]
);
$self->parseDoctrineStrings = true;
return $self;
}
public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\ConstExpr\ConstExprNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_FLOAT)) {
$value = $tokens->currentTokenValue();
$tokens->next();
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprFloatNode(str_replace('_', '', $value)),
$startLine,
$startIndex
);
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
$value = $tokens->currentTokenValue();
$tokens->next();
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $value)),
$startLine,
$startIndex
);
}
if ($this->parseDoctrineStrings && $tokens->isCurrentTokenType(Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) {
$value = $tokens->currentTokenValue();
$tokens->next();
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\DoctrineConstExprStringNode(Ast\ConstExpr\DoctrineConstExprStringNode::unescape($value)),
$startLine,
$startIndex
);
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING, Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
if ($this->parseDoctrineStrings) {
if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
throw new ParserException(
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Lexer::TOKEN_DOUBLE_QUOTED_STRING,
null,
$tokens->currentTokenLine()
);
}
$value = $tokens->currentTokenValue();
$tokens->next();
return $this->enrichWithAttributes(
$tokens,
$this->parseDoctrineString($value, $tokens),
$startLine,
$startIndex
);
}
$value = $tokens->currentTokenValue();
$type = $tokens->currentTokenType();
if ($trimStrings) {
if ($this->unescapeStrings) {
$value = StringUnescaper::unescapeString($value);
} else {
$value = substr($value, 1, -1);
}
}
$tokens->next();
if ($this->quoteAwareConstExprString) {
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\QuoteAwareConstExprStringNode(
$value,
$type === Lexer::TOKEN_SINGLE_QUOTED_STRING
? Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED
: Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED
),
$startLine,
$startIndex
);
}
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprStringNode($value),
$startLine,
$startIndex
);
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
$identifier = $tokens->currentTokenValue();
$tokens->next();
switch (strtolower($identifier)) {
case 'true':
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprTrueNode(),
$startLine,
$startIndex
);
case 'false':
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprFalseNode(),
$startLine,
$startIndex
);
case 'null':
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprNullNode(),
$startLine,
$startIndex
);
case 'array':
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_PARENTHESES, $startIndex);
}
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
$classConstantName = '';
$lastType = null;
while (true) {
if ($lastType !== Lexer::TOKEN_IDENTIFIER && $tokens->currentTokenType() === Lexer::TOKEN_IDENTIFIER) {
$classConstantName .= $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
$lastType = Lexer::TOKEN_IDENTIFIER;
continue;
}
if ($lastType !== Lexer::TOKEN_WILDCARD && $tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) {
$classConstantName .= '*';
$lastType = Lexer::TOKEN_WILDCARD;
if ($tokens->getSkippedHorizontalWhiteSpaceIfAny() !== '') {
break;
}
continue;
}
if ($lastType === null) {
// trigger parse error if nothing valid was consumed
$tokens->consumeTokenType(Lexer::TOKEN_WILDCARD);
}
break;
}
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstFetchNode($identifier, $classConstantName),
$startLine,
$startIndex
);
}
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstFetchNode('', $identifier),
$startLine,
$startIndex
);
} elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_SQUARE_BRACKET, $startIndex);
}
throw new ParserException(
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Lexer::TOKEN_IDENTIFIER,
null,
$tokens->currentTokenLine()
);
}
private function parseArray(TokenIterator $tokens, int $endToken, int $startIndex): Ast\ConstExpr\ConstExprArrayNode
{
$items = [];
$startLine = $tokens->currentTokenLine();
if (!$tokens->tryConsumeTokenType($endToken)) {
do {
$items[] = $this->parseArrayItem($tokens);
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA) && !$tokens->isCurrentTokenType($endToken));
$tokens->consumeTokenType($endToken);
}
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprArrayNode($items),
$startLine,
$startIndex
);
}
/**
* This method is supposed to be called with TokenIterator after reading TOKEN_DOUBLE_QUOTED_STRING and shifting
* to the next token.
*/
public function parseDoctrineString(string $text, TokenIterator $tokens): Ast\ConstExpr\DoctrineConstExprStringNode
{
// Because of how Lexer works, a valid Doctrine string
// can consist of a sequence of TOKEN_DOUBLE_QUOTED_STRING and TOKEN_DOCTRINE_ANNOTATION_STRING
while ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING, Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) {
$text .= $tokens->currentTokenValue();
$tokens->next();
}
return new Ast\ConstExpr\DoctrineConstExprStringNode(Ast\ConstExpr\DoctrineConstExprStringNode::unescape($text));
}
private function parseArrayItem(TokenIterator $tokens): Ast\ConstExpr\ConstExprArrayItemNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
$expr = $this->parse($tokens);
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_ARROW)) {
$key = $expr;
$value = $this->parse($tokens);
} else {
$key = null;
$value = $expr;
}
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprArrayItemNode($key, $value),
$startLine,
$startIndex
);
}
/**
* @template T of Ast\ConstExpr\ConstExprNode
* @param T $node
* @return T
*/
private function enrichWithAttributes(TokenIterator $tokens, Ast\ConstExpr\ConstExprNode $node, int $startLine, int $startIndex): Ast\ConstExpr\ConstExprNode
{
if ($this->useLinesAttributes) {
$node->setAttribute(Ast\Attribute::START_LINE, $startLine);
$node->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine());
}
if ($this->useIndexAttributes) {
$node->setAttribute(Ast\Attribute::START_INDEX, $startIndex);
$node->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken());
}
return $node;
}
}

View File

@ -0,0 +1,106 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Parser;
use Exception;
use PHPStan\PhpDocParser\Lexer\Lexer;
use function assert;
use function json_encode;
use function sprintf;
use const JSON_INVALID_UTF8_SUBSTITUTE;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;
class ParserException extends Exception
{
/** @var string */
private $currentTokenValue;
/** @var int */
private $currentTokenType;
/** @var int */
private $currentOffset;
/** @var int */
private $expectedTokenType;
/** @var string|null */
private $expectedTokenValue;
/** @var int|null */
private $currentTokenLine;
public function __construct(
string $currentTokenValue,
int $currentTokenType,
int $currentOffset,
int $expectedTokenType,
?string $expectedTokenValue = null,
?int $currentTokenLine = null
)
{
$this->currentTokenValue = $currentTokenValue;
$this->currentTokenType = $currentTokenType;
$this->currentOffset = $currentOffset;
$this->expectedTokenType = $expectedTokenType;
$this->expectedTokenValue = $expectedTokenValue;
$this->currentTokenLine = $currentTokenLine;
parent::__construct(sprintf(
'Unexpected token %s, expected %s%s at offset %d%s',
$this->formatValue($currentTokenValue),
Lexer::TOKEN_LABELS[$expectedTokenType],
$expectedTokenValue !== null ? sprintf(' (%s)', $this->formatValue($expectedTokenValue)) : '',
$currentOffset,
$currentTokenLine === null ? '' : sprintf(' on line %d', $currentTokenLine)
));
}
public function getCurrentTokenValue(): string
{
return $this->currentTokenValue;
}
public function getCurrentTokenType(): int
{
return $this->currentTokenType;
}
public function getCurrentOffset(): int
{
return $this->currentOffset;
}
public function getExpectedTokenType(): int
{
return $this->expectedTokenType;
}
public function getExpectedTokenValue(): ?string
{
return $this->expectedTokenValue;
}
public function getCurrentTokenLine(): ?int
{
return $this->currentTokenLine;
}
private function formatValue(string $value): string
{
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
assert($json !== false);
return $json;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,100 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Parser;
use PHPStan\ShouldNotHappenException;
use function chr;
use function hexdec;
use function octdec;
use function preg_replace_callback;
use function str_replace;
use function substr;
class StringUnescaper
{
private const REPLACEMENTS = [
'\\' => '\\',
'n' => "\n",
'r' => "\r",
't' => "\t",
'f' => "\f",
'v' => "\v",
'e' => "\x1B",
];
public static function unescapeString(string $string): string
{
$quote = $string[0];
if ($quote === '\'') {
return str_replace(
['\\\\', '\\\''],
['\\', '\''],
substr($string, 1, -1)
);
}
return self::parseEscapeSequences(substr($string, 1, -1), '"');
}
/**
* Implementation based on https://github.com/nikic/PHP-Parser/blob/b0edd4c41111042d43bb45c6c657b2e0db367d9e/lib/PhpParser/Node/Scalar/String_.php#L90-L130
*/
private static function parseEscapeSequences(string $str, string $quote): string
{
$str = str_replace('\\' . $quote, $quote, $str);
return preg_replace_callback(
'~\\\\([\\\\nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3}|u\{([0-9a-fA-F]+)\})~',
static function ($matches) {
$str = $matches[1];
if (isset(self::REPLACEMENTS[$str])) {
return self::REPLACEMENTS[$str];
}
if ($str[0] === 'x' || $str[0] === 'X') {
return chr((int) hexdec(substr($str, 1)));
}
if ($str[0] === 'u') {
if (!isset($matches[2])) {
throw new ShouldNotHappenException();
}
return self::codePointToUtf8((int) hexdec($matches[2]));
}
return chr((int) octdec($str));
},
$str
);
}
/**
* Implementation based on https://github.com/nikic/PHP-Parser/blob/b0edd4c41111042d43bb45c6c657b2e0db367d9e/lib/PhpParser/Node/Scalar/String_.php#L132-L154
*/
private static function codePointToUtf8(int $num): string
{
if ($num <= 0x7F) {
return chr($num);
}
if ($num <= 0x7FF) {
return chr(($num >> 6) + 0xC0)
. chr(($num & 0x3F) + 0x80);
}
if ($num <= 0xFFFF) {
return chr(($num >> 12) + 0xE0)
. chr((($num >> 6) & 0x3F) + 0x80)
. chr(($num & 0x3F) + 0x80);
}
if ($num <= 0x1FFFFF) {
return chr(($num >> 18) + 0xF0)
. chr((($num >> 12) & 0x3F) + 0x80)
. chr((($num >> 6) & 0x3F) + 0x80)
. chr(($num & 0x3F) + 0x80);
}
// Invalid UTF-8 codepoint escape sequence: Codepoint too large
return "\xef\xbf\xbd";
}
}

View File

@ -0,0 +1,383 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Parser;
use LogicException;
use PHPStan\PhpDocParser\Lexer\Lexer;
use function array_pop;
use function assert;
use function count;
use function in_array;
use function strlen;
use function substr;
class TokenIterator
{
/** @var list<array{string, int, int}> */
private $tokens;
/** @var int */
private $index;
/** @var int[] */
private $savePoints = [];
/** @var list<int> */
private $skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS];
/** @var string|null */
private $newline = null;
/**
* @param list<array{string, int, int}> $tokens
*/
public function __construct(array $tokens, int $index = 0)
{
$this->tokens = $tokens;
$this->index = $index;
$this->skipIrrelevantTokens();
}
/**
* @return list<array{string, int, int}>
*/
public function getTokens(): array
{
return $this->tokens;
}
public function getContentBetween(int $startPos, int $endPos): string
{
if ($startPos < 0 || $endPos > count($this->tokens)) {
throw new LogicException();
}
$content = '';
for ($i = $startPos; $i < $endPos; $i++) {
$content .= $this->tokens[$i][Lexer::VALUE_OFFSET];
}
return $content;
}
public function getTokenCount(): int
{
return count($this->tokens);
}
public function currentTokenValue(): string
{
return $this->tokens[$this->index][Lexer::VALUE_OFFSET];
}
public function currentTokenType(): int
{
return $this->tokens[$this->index][Lexer::TYPE_OFFSET];
}
public function currentTokenOffset(): int
{
$offset = 0;
for ($i = 0; $i < $this->index; $i++) {
$offset += strlen($this->tokens[$i][Lexer::VALUE_OFFSET]);
}
return $offset;
}
public function currentTokenLine(): int
{
return $this->tokens[$this->index][Lexer::LINE_OFFSET];
}
public function currentTokenIndex(): int
{
return $this->index;
}
public function endIndexOfLastRelevantToken(): int
{
$endIndex = $this->currentTokenIndex();
$endIndex--;
while (in_array($this->tokens[$endIndex][Lexer::TYPE_OFFSET], $this->skippedTokenTypes, true)) {
if (!isset($this->tokens[$endIndex - 1])) {
break;
}
$endIndex--;
}
return $endIndex;
}
public function isCurrentTokenValue(string $tokenValue): bool
{
return $this->tokens[$this->index][Lexer::VALUE_OFFSET] === $tokenValue;
}
public function isCurrentTokenType(int ...$tokenType): bool
{
return in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $tokenType, true);
}
public function isPrecededByHorizontalWhitespace(): bool
{
return ($this->tokens[$this->index - 1][Lexer::TYPE_OFFSET] ?? -1) === Lexer::TOKEN_HORIZONTAL_WS;
}
/**
* @throws ParserException
*/
public function consumeTokenType(int $tokenType): void
{
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType) {
$this->throwError($tokenType);
}
if ($tokenType === Lexer::TOKEN_PHPDOC_EOL) {
if ($this->newline === null) {
$this->detectNewline();
}
}
$this->index++;
$this->skipIrrelevantTokens();
}
/**
* @throws ParserException
*/
public function consumeTokenValue(int $tokenType, string $tokenValue): void
{
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType || $this->tokens[$this->index][Lexer::VALUE_OFFSET] !== $tokenValue) {
$this->throwError($tokenType, $tokenValue);
}
$this->index++;
$this->skipIrrelevantTokens();
}
/** @phpstan-impure */
public function tryConsumeTokenValue(string $tokenValue): bool
{
if ($this->tokens[$this->index][Lexer::VALUE_OFFSET] !== $tokenValue) {
return false;
}
$this->index++;
$this->skipIrrelevantTokens();
return true;
}
/** @phpstan-impure */
public function tryConsumeTokenType(int $tokenType): bool
{
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType) {
return false;
}
if ($tokenType === Lexer::TOKEN_PHPDOC_EOL) {
if ($this->newline === null) {
$this->detectNewline();
}
}
$this->index++;
$this->skipIrrelevantTokens();
return true;
}
private function detectNewline(): void
{
$value = $this->currentTokenValue();
if (substr($value, 0, 2) === "\r\n") {
$this->newline = "\r\n";
} elseif (substr($value, 0, 1) === "\n") {
$this->newline = "\n";
}
}
public function getSkippedHorizontalWhiteSpaceIfAny(): string
{
if ($this->index > 0 && $this->tokens[$this->index - 1][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) {
return $this->tokens[$this->index - 1][Lexer::VALUE_OFFSET];
}
return '';
}
/** @phpstan-impure */
public function joinUntil(int ...$tokenType): string
{
$s = '';
while (!in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $tokenType, true)) {
$s .= $this->tokens[$this->index++][Lexer::VALUE_OFFSET];
}
return $s;
}
public function next(): void
{
$this->index++;
$this->skipIrrelevantTokens();
}
private function skipIrrelevantTokens(): void
{
if (!isset($this->tokens[$this->index])) {
return;
}
while (in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $this->skippedTokenTypes, true)) {
if (!isset($this->tokens[$this->index + 1])) {
break;
}
$this->index++;
}
}
public function addEndOfLineToSkippedTokens(): void
{
$this->skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS, Lexer::TOKEN_PHPDOC_EOL];
}
public function removeEndOfLineFromSkippedTokens(): void
{
$this->skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS];
}
/** @phpstan-impure */
public function forwardToTheEnd(): void
{
$lastToken = count($this->tokens) - 1;
$this->index = $lastToken;
}
public function pushSavePoint(): void
{
$this->savePoints[] = $this->index;
}
public function dropSavePoint(): void
{
array_pop($this->savePoints);
}
public function rollback(): void
{
$index = array_pop($this->savePoints);
assert($index !== null);
$this->index = $index;
}
/**
* @throws ParserException
*/
private function throwError(int $expectedTokenType, ?string $expectedTokenValue = null): void
{
throw new ParserException(
$this->currentTokenValue(),
$this->currentTokenType(),
$this->currentTokenOffset(),
$expectedTokenType,
$expectedTokenValue,
$this->currentTokenLine()
);
}
/**
* Check whether the position is directly preceded by a certain token type.
*
* During this check TOKEN_HORIZONTAL_WS and TOKEN_PHPDOC_EOL are skipped
*/
public function hasTokenImmediatelyBefore(int $pos, int $expectedTokenType): bool
{
$tokens = $this->tokens;
$pos--;
for (; $pos >= 0; $pos--) {
$token = $tokens[$pos];
$type = $token[Lexer::TYPE_OFFSET];
if ($type === $expectedTokenType) {
return true;
}
if (!in_array($type, [
Lexer::TOKEN_HORIZONTAL_WS,
Lexer::TOKEN_PHPDOC_EOL,
], true)) {
break;
}
}
return false;
}
/**
* Check whether the position is directly followed by a certain token type.
*
* During this check TOKEN_HORIZONTAL_WS and TOKEN_PHPDOC_EOL are skipped
*/
public function hasTokenImmediatelyAfter(int $pos, int $expectedTokenType): bool
{
$tokens = $this->tokens;
$pos++;
for ($c = count($tokens); $pos < $c; $pos++) {
$token = $tokens[$pos];
$type = $token[Lexer::TYPE_OFFSET];
if ($type === $expectedTokenType) {
return true;
}
if (!in_array($type, [
Lexer::TOKEN_HORIZONTAL_WS,
Lexer::TOKEN_PHPDOC_EOL,
], true)) {
break;
}
}
return false;
}
public function getDetectedNewline(): ?string
{
return $this->newline;
}
/**
* Whether the given position is immediately surrounded by parenthesis.
*/
public function hasParentheses(int $startPos, int $endPos): bool
{
return $this->hasTokenImmediatelyBefore($startPos, Lexer::TOKEN_OPEN_PARENTHESES)
&& $this->hasTokenImmediatelyAfter($endPos, Lexer::TOKEN_CLOSE_PARENTHESES);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,44 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Printer;
/**
* Inspired by https://github.com/nikic/PHP-Parser/tree/36a6dcd04e7b0285e8f0868f44bd4927802f7df1
*
* Copyright (c) 2011, Nikita Popov
* All rights reserved.
*
* Implements the Myers diff algorithm.
*
* @internal
*/
class DiffElem
{
public const TYPE_KEEP = 0;
public const TYPE_REMOVE = 1;
public const TYPE_ADD = 2;
public const TYPE_REPLACE = 3;
/** @var self::TYPE_* */
public $type;
/** @var mixed Is null for add operations */
public $old;
/** @var mixed Is null for remove operations */
public $new;
/**
* @param self::TYPE_* $type
* @param mixed $old Is null for add operations
* @param mixed $new Is null for remove operations
*/
public function __construct(int $type, $old, $new)
{
$this->type = $type;
$this->old = $old;
$this->new = $new;
}
}

View File

@ -0,0 +1,196 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Printer;
use Exception;
use function array_reverse;
use function count;
/**
* Inspired by https://github.com/nikic/PHP-Parser/tree/36a6dcd04e7b0285e8f0868f44bd4927802f7df1
*
* Copyright (c) 2011, Nikita Popov
* All rights reserved.
*
* Implements the Myers diff algorithm.
*
* Myers, Eugene W. "An O (ND) difference algorithm and its variations."
* Algorithmica 1.1 (1986): 251-266.
*
* @template T
* @internal
*/
class Differ
{
/** @var callable(T, T): bool */
private $isEqual;
/**
* Create differ over the given equality relation.
*
* @param callable(T, T): bool $isEqual Equality relation
*/
public function __construct(callable $isEqual)
{
$this->isEqual = $isEqual;
}
/**
* Calculate diff (edit script) from $old to $new.
*
* @param T[] $old Original array
* @param T[] $new New array
*
* @return DiffElem[] Diff (edit script)
*/
public function diff(array $old, array $new): array
{
[$trace, $x, $y] = $this->calculateTrace($old, $new);
return $this->extractDiff($trace, $x, $y, $old, $new);
}
/**
* Calculate diff, including "replace" operations.
*
* If a sequence of remove operations is followed by the same number of add operations, these
* will be coalesced into replace operations.
*
* @param T[] $old Original array
* @param T[] $new New array
*
* @return DiffElem[] Diff (edit script), including replace operations
*/
public function diffWithReplacements(array $old, array $new): array
{
return $this->coalesceReplacements($this->diff($old, $new));
}
/**
* @param T[] $old
* @param T[] $new
* @return array{array<int, array<int, int>>, int, int}
*/
private function calculateTrace(array $old, array $new): array
{
$n = count($old);
$m = count($new);
$max = $n + $m;
$v = [1 => 0];
$trace = [];
for ($d = 0; $d <= $max; $d++) {
$trace[] = $v;
for ($k = -$d; $k <= $d; $k += 2) {
if ($k === -$d || ($k !== $d && $v[$k - 1] < $v[$k + 1])) {
$x = $v[$k + 1];
} else {
$x = $v[$k - 1] + 1;
}
$y = $x - $k;
while ($x < $n && $y < $m && ($this->isEqual)($old[$x], $new[$y])) {
$x++;
$y++;
}
$v[$k] = $x;
if ($x >= $n && $y >= $m) {
return [$trace, $x, $y];
}
}
}
throw new Exception('Should not happen');
}
/**
* @param array<int, array<int, int>> $trace
* @param T[] $old
* @param T[] $new
* @return DiffElem[]
*/
private function extractDiff(array $trace, int $x, int $y, array $old, array $new): array
{
$result = [];
for ($d = count($trace) - 1; $d >= 0; $d--) {
$v = $trace[$d];
$k = $x - $y;
if ($k === -$d || ($k !== $d && $v[$k - 1] < $v[$k + 1])) {
$prevK = $k + 1;
} else {
$prevK = $k - 1;
}
$prevX = $v[$prevK];
$prevY = $prevX - $prevK;
while ($x > $prevX && $y > $prevY) {
$result[] = new DiffElem(DiffElem::TYPE_KEEP, $old[$x - 1], $new[$y - 1]);
$x--;
$y--;
}
if ($d === 0) {
break;
}
while ($x > $prevX) {
$result[] = new DiffElem(DiffElem::TYPE_REMOVE, $old[$x - 1], null);
$x--;
}
while ($y > $prevY) {
$result[] = new DiffElem(DiffElem::TYPE_ADD, null, $new[$y - 1]);
$y--;
}
}
return array_reverse($result);
}
/**
* Coalesce equal-length sequences of remove+add into a replace operation.
*
* @param DiffElem[] $diff
* @return DiffElem[]
*/
private function coalesceReplacements(array $diff): array
{
$newDiff = [];
$c = count($diff);
for ($i = 0; $i < $c; $i++) {
$diffType = $diff[$i]->type;
if ($diffType !== DiffElem::TYPE_REMOVE) {
$newDiff[] = $diff[$i];
continue;
}
$j = $i;
while ($j < $c && $diff[$j]->type === DiffElem::TYPE_REMOVE) {
$j++;
}
$k = $j;
while ($k < $c && $diff[$k]->type === DiffElem::TYPE_ADD) {
$k++;
}
if ($j - $i === $k - $j) {
$len = $j - $i;
for ($n = 0; $n < $len; $n++) {
$newDiff[] = new DiffElem(
DiffElem::TYPE_REPLACE,
$diff[$i + $n]->old,
$diff[$j + $n]->new
);
}
} else {
for (; $i < $k; $i++) {
$newDiff[] = $diff[$i];
}
}
$i = $k - 1;
}
return $newDiff;
}
}

View File

@ -0,0 +1,866 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Printer;
use LogicException;
use PHPStan\PhpDocParser\Ast\Attribute;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagMethodValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagPropertyValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineAnnotation;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArgument;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArray;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArrayItem;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ImplementsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamClosureThisTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamOutTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasImportTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\UsesTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeUnsealedTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
use PHPStan\PhpDocParser\Ast\Type\InvalidTypeNode;
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode;
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use function array_keys;
use function array_map;
use function count;
use function get_class;
use function get_object_vars;
use function implode;
use function in_array;
use function is_array;
use function preg_match_all;
use function sprintf;
use function strlen;
use function strpos;
use function trim;
use const PREG_SET_ORDER;
/**
* Inspired by https://github.com/nikic/PHP-Parser/tree/36a6dcd04e7b0285e8f0868f44bd4927802f7df1
*
* Copyright (c) 2011, Nikita Popov
* All rights reserved.
*/
final class Printer
{
/** @var Differ<Node> */
private $differ;
/**
* Map From "{$class}->{$subNode}" to string that should be inserted
* between elements of this list subnode
*
* @var array<string, string>
*/
private $listInsertionMap = [
PhpDocNode::class . '->children' => "\n * ",
UnionTypeNode::class . '->types' => '|',
IntersectionTypeNode::class . '->types' => '&',
ArrayShapeNode::class . '->items' => ', ',
ObjectShapeNode::class . '->items' => ', ',
CallableTypeNode::class . '->parameters' => ', ',
CallableTypeNode::class . '->templateTypes' => ', ',
GenericTypeNode::class . '->genericTypes' => ', ',
ConstExprArrayNode::class . '->items' => ', ',
MethodTagValueNode::class . '->parameters' => ', ',
DoctrineArray::class . '->items' => ', ',
DoctrineAnnotation::class . '->arguments' => ', ',
];
/**
* [$find, $extraLeft, $extraRight]
*
* @var array<string, array{string|null, string, string}>
*/
private $emptyListInsertionMap = [
CallableTypeNode::class . '->parameters' => ['(', '', ''],
ArrayShapeNode::class . '->items' => ['{', '', ''],
ObjectShapeNode::class . '->items' => ['{', '', ''],
DoctrineArray::class . '->items' => ['{', '', ''],
DoctrineAnnotation::class . '->arguments' => ['(', '', ''],
];
/** @var array<string, list<class-string<TypeNode>>> */
private $parenthesesMap = [
CallableTypeNode::class . '->returnType' => [
CallableTypeNode::class,
UnionTypeNode::class,
IntersectionTypeNode::class,
],
ArrayTypeNode::class . '->type' => [
CallableTypeNode::class,
UnionTypeNode::class,
IntersectionTypeNode::class,
ConstTypeNode::class,
NullableTypeNode::class,
],
OffsetAccessTypeNode::class . '->type' => [
CallableTypeNode::class,
UnionTypeNode::class,
IntersectionTypeNode::class,
NullableTypeNode::class,
],
];
/** @var array<string, list<class-string<TypeNode>>> */
private $parenthesesListMap = [
IntersectionTypeNode::class . '->types' => [
IntersectionTypeNode::class,
UnionTypeNode::class,
NullableTypeNode::class,
],
UnionTypeNode::class . '->types' => [
IntersectionTypeNode::class,
UnionTypeNode::class,
NullableTypeNode::class,
],
];
public function printFormatPreserving(PhpDocNode $node, PhpDocNode $originalNode, TokenIterator $originalTokens): string
{
$this->differ = new Differ(static function ($a, $b) {
if ($a instanceof Node && $b instanceof Node) {
return $a === $b->getAttribute(Attribute::ORIGINAL_NODE);
}
return false;
});
$tokenIndex = 0;
$result = $this->printArrayFormatPreserving(
$node->children,
$originalNode->children,
$originalTokens,
$tokenIndex,
PhpDocNode::class,
'children'
);
if ($result !== null) {
return $result . $originalTokens->getContentBetween($tokenIndex, $originalTokens->getTokenCount());
}
return $this->print($node);
}
public function print(Node $node): string
{
if ($node instanceof PhpDocNode) {
return "/**\n *" . implode("\n *", array_map(
function (PhpDocChildNode $child): string {
$s = $this->print($child);
return $s === '' ? '' : ' ' . $s;
},
$node->children
)) . "\n */";
}
if ($node instanceof PhpDocTextNode) {
return $node->text;
}
if ($node instanceof PhpDocTagNode) {
if ($node->value instanceof DoctrineTagValueNode) {
return $this->print($node->value);
}
return trim(sprintf('%s %s', $node->name, $this->print($node->value)));
}
if ($node instanceof PhpDocTagValueNode) {
return $this->printTagValue($node);
}
if ($node instanceof TypeNode) {
return $this->printType($node);
}
if ($node instanceof ConstExprNode) {
return $this->printConstExpr($node);
}
if ($node instanceof MethodTagValueParameterNode) {
$type = $node->type !== null ? $this->print($node->type) . ' ' : '';
$isReference = $node->isReference ? '&' : '';
$isVariadic = $node->isVariadic ? '...' : '';
$default = $node->defaultValue !== null ? ' = ' . $this->print($node->defaultValue) : '';
return "{$type}{$isReference}{$isVariadic}{$node->parameterName}{$default}";
}
if ($node instanceof CallableTypeParameterNode) {
$type = $this->print($node->type) . ' ';
$isReference = $node->isReference ? '&' : '';
$isVariadic = $node->isVariadic ? '...' : '';
$isOptional = $node->isOptional ? '=' : '';
return trim("{$type}{$isReference}{$isVariadic}{$node->parameterName}") . $isOptional;
}
if ($node instanceof ArrayShapeUnsealedTypeNode) {
if ($node->keyType !== null) {
return sprintf('<%s, %s>', $this->printType($node->keyType), $this->printType($node->valueType));
}
return sprintf('<%s>', $this->printType($node->valueType));
}
if ($node instanceof DoctrineAnnotation) {
return (string) $node;
}
if ($node instanceof DoctrineArgument) {
return (string) $node;
}
if ($node instanceof DoctrineArray) {
return (string) $node;
}
if ($node instanceof DoctrineArrayItem) {
return (string) $node;
}
throw new LogicException(sprintf('Unknown node type %s', get_class($node)));
}
private function printTagValue(PhpDocTagValueNode $node): string
{
// only nodes that contain another node are handled here
// the rest falls back on (string) $node
if ($node instanceof AssertTagMethodValueNode) {
$isNegated = $node->isNegated ? '!' : '';
$isEquality = $node->isEquality ? '=' : '';
$type = $this->printType($node->type);
return trim("{$isNegated}{$isEquality}{$type} {$node->parameter}->{$node->method}() {$node->description}");
}
if ($node instanceof AssertTagPropertyValueNode) {
$isNegated = $node->isNegated ? '!' : '';
$isEquality = $node->isEquality ? '=' : '';
$type = $this->printType($node->type);
return trim("{$isNegated}{$isEquality}{$type} {$node->parameter}->{$node->property} {$node->description}");
}
if ($node instanceof AssertTagValueNode) {
$isNegated = $node->isNegated ? '!' : '';
$isEquality = $node->isEquality ? '=' : '';
$type = $this->printType($node->type);
return trim("{$isNegated}{$isEquality}{$type} {$node->parameter} {$node->description}");
}
if ($node instanceof ExtendsTagValueNode || $node instanceof ImplementsTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->description}");
}
if ($node instanceof MethodTagValueNode) {
$static = $node->isStatic ? 'static ' : '';
$returnType = $node->returnType !== null ? $this->printType($node->returnType) . ' ' : '';
$parameters = implode(', ', array_map(function (MethodTagValueParameterNode $parameter): string {
return $this->print($parameter);
}, $node->parameters));
$description = $node->description !== '' ? " {$node->description}" : '';
$templateTypes = count($node->templateTypes) > 0 ? '<' . implode(', ', array_map(function (TemplateTagValueNode $templateTag): string {
return $this->print($templateTag);
}, $node->templateTypes)) . '>' : '';
return "{$static}{$returnType}{$node->methodName}{$templateTypes}({$parameters}){$description}";
}
if ($node instanceof MixinTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->description}");
}
if ($node instanceof RequireExtendsTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->description}");
}
if ($node instanceof RequireImplementsTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->description}");
}
if ($node instanceof ParamOutTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->parameterName} {$node->description}");
}
if ($node instanceof ParamTagValueNode) {
$reference = $node->isReference ? '&' : '';
$variadic = $node->isVariadic ? '...' : '';
$type = $this->printType($node->type);
return trim("{$type} {$reference}{$variadic}{$node->parameterName} {$node->description}");
}
if ($node instanceof ParamImmediatelyInvokedCallableTagValueNode) {
return trim("{$node->parameterName} {$node->description}");
}
if ($node instanceof ParamLaterInvokedCallableTagValueNode) {
return trim("{$node->parameterName} {$node->description}");
}
if ($node instanceof ParamClosureThisTagValueNode) {
return trim("{$node->type} {$node->parameterName} {$node->description}");
}
if ($node instanceof PropertyTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->propertyName} {$node->description}");
}
if ($node instanceof ReturnTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->description}");
}
if ($node instanceof SelfOutTagValueNode) {
$type = $this->printType($node->type);
return trim($type . ' ' . $node->description);
}
if ($node instanceof TemplateTagValueNode) {
$upperBound = $node->bound !== null ? ' of ' . $this->printType($node->bound) : '';
$lowerBound = $node->lowerBound !== null ? ' super ' . $this->printType($node->lowerBound) : '';
$default = $node->default !== null ? ' = ' . $this->printType($node->default) : '';
return trim("{$node->name}{$upperBound}{$lowerBound}{$default} {$node->description}");
}
if ($node instanceof ThrowsTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->description}");
}
if ($node instanceof TypeAliasImportTagValueNode) {
return trim(
"{$node->importedAlias} from " . $this->printType($node->importedFrom)
. ($node->importedAs !== null ? " as {$node->importedAs}" : '')
);
}
if ($node instanceof TypeAliasTagValueNode) {
$type = $this->printType($node->type);
return trim("{$node->alias} {$type}");
}
if ($node instanceof UsesTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->description}");
}
if ($node instanceof VarTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} " . trim("{$node->variableName} {$node->description}"));
}
return (string) $node;
}
private function printType(TypeNode $node): string
{
if ($node instanceof ArrayShapeNode) {
$items = array_map(function (ArrayShapeItemNode $item): string {
return $this->printType($item);
}, $node->items);
if (! $node->sealed) {
$items[] = '...' . ($node->unsealedType === null ? '' : $this->print($node->unsealedType));
}
return $node->kind . '{' . implode(', ', $items) . '}';
}
if ($node instanceof ArrayShapeItemNode) {
if ($node->keyName !== null) {
return sprintf(
'%s%s: %s',
$this->print($node->keyName),
$node->optional ? '?' : '',
$this->printType($node->valueType)
);
}
return $this->printType($node->valueType);
}
if ($node instanceof ArrayTypeNode) {
return $this->printOffsetAccessType($node->type) . '[]';
}
if ($node instanceof CallableTypeNode) {
if ($node->returnType instanceof CallableTypeNode || $node->returnType instanceof UnionTypeNode || $node->returnType instanceof IntersectionTypeNode) {
$returnType = $this->wrapInParentheses($node->returnType);
} else {
$returnType = $this->printType($node->returnType);
}
$template = $node->templateTypes !== []
? '<' . implode(', ', array_map(function (TemplateTagValueNode $templateNode): string {
return $this->print($templateNode);
}, $node->templateTypes)) . '>'
: '';
$parameters = implode(', ', array_map(function (CallableTypeParameterNode $parameterNode): string {
return $this->print($parameterNode);
}, $node->parameters));
return "{$node->identifier}{$template}({$parameters}): {$returnType}";
}
if ($node instanceof ConditionalTypeForParameterNode) {
return sprintf(
'(%s %s %s ? %s : %s)',
$node->parameterName,
$node->negated ? 'is not' : 'is',
$this->printType($node->targetType),
$this->printType($node->if),
$this->printType($node->else)
);
}
if ($node instanceof ConditionalTypeNode) {
return sprintf(
'(%s %s %s ? %s : %s)',
$this->printType($node->subjectType),
$node->negated ? 'is not' : 'is',
$this->printType($node->targetType),
$this->printType($node->if),
$this->printType($node->else)
);
}
if ($node instanceof ConstTypeNode) {
return $this->printConstExpr($node->constExpr);
}
if ($node instanceof GenericTypeNode) {
$genericTypes = [];
foreach ($node->genericTypes as $index => $type) {
$variance = $node->variances[$index] ?? GenericTypeNode::VARIANCE_INVARIANT;
if ($variance === GenericTypeNode::VARIANCE_INVARIANT) {
$genericTypes[] = $this->printType($type);
} elseif ($variance === GenericTypeNode::VARIANCE_BIVARIANT) {
$genericTypes[] = '*';
} else {
$genericTypes[] = sprintf('%s %s', $variance, $this->print($type));
}
}
return $node->type . '<' . implode(', ', $genericTypes) . '>';
}
if ($node instanceof IdentifierTypeNode) {
return $node->name;
}
if ($node instanceof IntersectionTypeNode || $node instanceof UnionTypeNode) {
$items = [];
foreach ($node->types as $type) {
if (
$type instanceof IntersectionTypeNode
|| $type instanceof UnionTypeNode
|| $type instanceof NullableTypeNode
) {
$items[] = $this->wrapInParentheses($type);
continue;
}
$items[] = $this->printType($type);
}
return implode($node instanceof IntersectionTypeNode ? '&' : '|', $items);
}
if ($node instanceof InvalidTypeNode) {
return (string) $node;
}
if ($node instanceof NullableTypeNode) {
if ($node->type instanceof IntersectionTypeNode || $node->type instanceof UnionTypeNode) {
return '?(' . $this->printType($node->type) . ')';
}
return '?' . $this->printType($node->type);
}
if ($node instanceof ObjectShapeNode) {
$items = array_map(function (ObjectShapeItemNode $item): string {
return $this->printType($item);
}, $node->items);
return 'object{' . implode(', ', $items) . '}';
}
if ($node instanceof ObjectShapeItemNode) {
if ($node->keyName !== null) {
return sprintf(
'%s%s: %s',
$this->print($node->keyName),
$node->optional ? '?' : '',
$this->printType($node->valueType)
);
}
return $this->printType($node->valueType);
}
if ($node instanceof OffsetAccessTypeNode) {
return $this->printOffsetAccessType($node->type) . '[' . $this->printType($node->offset) . ']';
}
if ($node instanceof ThisTypeNode) {
return (string) $node;
}
throw new LogicException(sprintf('Unknown node type %s', get_class($node)));
}
private function wrapInParentheses(TypeNode $node): string
{
return '(' . $this->printType($node) . ')';
}
private function printOffsetAccessType(TypeNode $type): string
{
if (
$type instanceof CallableTypeNode
|| $type instanceof UnionTypeNode
|| $type instanceof IntersectionTypeNode
|| $type instanceof NullableTypeNode
) {
return $this->wrapInParentheses($type);
}
return $this->printType($type);
}
private function printConstExpr(ConstExprNode $node): string
{
// this is fine - ConstExprNode classes do not contain nodes that need smart printer logic
return (string) $node;
}
/**
* @param Node[] $nodes
* @param Node[] $originalNodes
*/
private function printArrayFormatPreserving(array $nodes, array $originalNodes, TokenIterator $originalTokens, int &$tokenIndex, string $parentNodeClass, string $subNodeName): ?string
{
$diff = $this->differ->diffWithReplacements($originalNodes, $nodes);
$mapKey = $parentNodeClass . '->' . $subNodeName;
$insertStr = $this->listInsertionMap[$mapKey] ?? null;
$result = '';
$beforeFirstKeepOrReplace = true;
$delayedAdd = [];
$insertNewline = false;
[$isMultiline, $beforeAsteriskIndent, $afterAsteriskIndent] = $this->isMultiline($tokenIndex, $originalNodes, $originalTokens);
if ($insertStr === "\n * ") {
$insertStr = sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
}
foreach ($diff as $i => $diffElem) {
$diffType = $diffElem->type;
$newNode = $diffElem->new;
$originalNode = $diffElem->old;
if ($diffType === DiffElem::TYPE_KEEP || $diffType === DiffElem::TYPE_REPLACE) {
$beforeFirstKeepOrReplace = false;
if (!$newNode instanceof Node || !$originalNode instanceof Node) {
return null;
}
$itemStartPos = $originalNode->getAttribute(Attribute::START_INDEX);
$itemEndPos = $originalNode->getAttribute(Attribute::END_INDEX);
if ($itemStartPos < 0 || $itemEndPos < 0 || $itemStartPos < $tokenIndex) {
throw new LogicException();
}
$result .= $originalTokens->getContentBetween($tokenIndex, $itemStartPos);
if (count($delayedAdd) > 0) {
foreach ($delayedAdd as $delayedAddNode) {
$parenthesesNeeded = isset($this->parenthesesListMap[$mapKey])
&& in_array(get_class($delayedAddNode), $this->parenthesesListMap[$mapKey], true);
if ($parenthesesNeeded) {
$result .= '(';
}
$result .= $this->printNodeFormatPreserving($delayedAddNode, $originalTokens);
if ($parenthesesNeeded) {
$result .= ')';
}
if ($insertNewline) {
$result .= $insertStr . sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
} else {
$result .= $insertStr;
}
}
$delayedAdd = [];
}
$parenthesesNeeded = isset($this->parenthesesListMap[$mapKey])
&& in_array(get_class($newNode), $this->parenthesesListMap[$mapKey], true)
&& !in_array(get_class($originalNode), $this->parenthesesListMap[$mapKey], true);
$addParentheses = $parenthesesNeeded && !$originalTokens->hasParentheses($itemStartPos, $itemEndPos);
if ($addParentheses) {
$result .= '(';
}
$result .= $this->printNodeFormatPreserving($newNode, $originalTokens);
if ($addParentheses) {
$result .= ')';
}
$tokenIndex = $itemEndPos + 1;
} elseif ($diffType === DiffElem::TYPE_ADD) {
if ($insertStr === null) {
return null;
}
if (!$newNode instanceof Node) {
return null;
}
if ($insertStr === ', ' && $isMultiline) {
$insertStr = ',';
$insertNewline = true;
}
if ($beforeFirstKeepOrReplace) {
// Will be inserted at the next "replace" or "keep" element
$delayedAdd[] = $newNode;
continue;
}
$itemEndPos = $tokenIndex - 1;
if ($insertNewline) {
$result .= $insertStr . sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
} else {
$result .= $insertStr;
}
$parenthesesNeeded = isset($this->parenthesesListMap[$mapKey])
&& in_array(get_class($newNode), $this->parenthesesListMap[$mapKey], true);
if ($parenthesesNeeded) {
$result .= '(';
}
$result .= $this->printNodeFormatPreserving($newNode, $originalTokens);
if ($parenthesesNeeded) {
$result .= ')';
}
$tokenIndex = $itemEndPos + 1;
} elseif ($diffType === DiffElem::TYPE_REMOVE) {
if (!$originalNode instanceof Node) {
return null;
}
$itemStartPos = $originalNode->getAttribute(Attribute::START_INDEX);
$itemEndPos = $originalNode->getAttribute(Attribute::END_INDEX);
if ($itemStartPos < 0 || $itemEndPos < 0) {
throw new LogicException();
}
if ($i === 0) {
// If we're removing from the start, keep the tokens before the node and drop those after it,
// instead of the other way around.
$originalTokensArray = $originalTokens->getTokens();
for ($j = $tokenIndex; $j < $itemStartPos; $j++) {
if ($originalTokensArray[$j][Lexer::TYPE_OFFSET] === Lexer::TOKEN_PHPDOC_EOL) {
break;
}
$result .= $originalTokensArray[$j][Lexer::VALUE_OFFSET];
}
}
$tokenIndex = $itemEndPos + 1;
}
}
if (count($delayedAdd) > 0) {
if (!isset($this->emptyListInsertionMap[$mapKey])) {
return null;
}
[$findToken, $extraLeft, $extraRight] = $this->emptyListInsertionMap[$mapKey];
if ($findToken !== null) {
$originalTokensArray = $originalTokens->getTokens();
for (; $tokenIndex < count($originalTokensArray); $tokenIndex++) {
$result .= $originalTokensArray[$tokenIndex][Lexer::VALUE_OFFSET];
if ($originalTokensArray[$tokenIndex][Lexer::VALUE_OFFSET] !== $findToken) {
continue;
}
$tokenIndex++;
break;
}
}
$first = true;
$result .= $extraLeft;
foreach ($delayedAdd as $delayedAddNode) {
if (!$first) {
$result .= $insertStr;
if ($insertNewline) {
$result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
}
}
$result .= $this->printNodeFormatPreserving($delayedAddNode, $originalTokens);
$first = false;
}
$result .= $extraRight;
}
return $result;
}
/**
* @param Node[] $nodes
* @return array{bool, string, string}
*/
private function isMultiline(int $initialIndex, array $nodes, TokenIterator $originalTokens): array
{
$isMultiline = count($nodes) > 1;
$pos = $initialIndex;
$allText = '';
/** @var Node|null $node */
foreach ($nodes as $node) {
if (!$node instanceof Node) {
continue;
}
$endPos = $node->getAttribute(Attribute::END_INDEX) + 1;
$text = $originalTokens->getContentBetween($pos, $endPos);
$allText .= $text;
if (strpos($text, "\n") === false) {
// We require that a newline is present between *every* item. If the formatting
// is inconsistent, with only some items having newlines, we don't consider it
// as multiline
$isMultiline = false;
}
$pos = $endPos;
}
$c = preg_match_all('~\n(?<before>[\\x09\\x20]*)\*(?<after>\\x20*)~', $allText, $matches, PREG_SET_ORDER);
if ($c === 0) {
return [$isMultiline, '', ''];
}
$before = '';
$after = '';
foreach ($matches as $match) {
if (strlen($match['before']) > strlen($before)) {
$before = $match['before'];
}
if (strlen($match['after']) <= strlen($after)) {
continue;
}
$after = $match['after'];
}
return [$isMultiline, $before, $after];
}
private function printNodeFormatPreserving(Node $node, TokenIterator $originalTokens): string
{
/** @var Node|null $originalNode */
$originalNode = $node->getAttribute(Attribute::ORIGINAL_NODE);
if ($originalNode === null) {
return $this->print($node);
}
$class = get_class($node);
if ($class !== get_class($originalNode)) {
throw new LogicException();
}
$startPos = $originalNode->getAttribute(Attribute::START_INDEX);
$endPos = $originalNode->getAttribute(Attribute::END_INDEX);
if ($startPos < 0 || $endPos < 0) {
throw new LogicException();
}
$result = '';
$pos = $startPos;
$subNodeNames = array_keys(get_object_vars($node));
foreach ($subNodeNames as $subNodeName) {
$subNode = $node->$subNodeName;
$origSubNode = $originalNode->$subNodeName;
if (
(!$subNode instanceof Node && $subNode !== null)
|| (!$origSubNode instanceof Node && $origSubNode !== null)
) {
if ($subNode === $origSubNode) {
// Unchanged, can reuse old code
continue;
}
if (is_array($subNode) && is_array($origSubNode)) {
// Array subnode changed, we might be able to reconstruct it
$listResult = $this->printArrayFormatPreserving(
$subNode,
$origSubNode,
$originalTokens,
$pos,
$class,
$subNodeName
);
if ($listResult === null) {
return $this->print($node);
}
$result .= $listResult;
continue;
}
return $this->print($node);
}
if ($origSubNode === null) {
if ($subNode === null) {
// Both null, nothing to do
continue;
}
return $this->print($node);
}
$subStartPos = $origSubNode->getAttribute(Attribute::START_INDEX);
$subEndPos = $origSubNode->getAttribute(Attribute::END_INDEX);
if ($subStartPos < 0 || $subEndPos < 0) {
throw new LogicException();
}
if ($subEndPos < $subStartPos) {
return $this->print($node);
}
if ($subNode === null) {
return $this->print($node);
}
$result .= $originalTokens->getContentBetween($pos, $subStartPos);
$mapKey = get_class($node) . '->' . $subNodeName;
$parenthesesNeeded = isset($this->parenthesesMap[$mapKey])
&& in_array(get_class($subNode), $this->parenthesesMap[$mapKey], true);
if ($subNode->getAttribute(Attribute::ORIGINAL_NODE) !== null) {
$parenthesesNeeded = $parenthesesNeeded
&& !in_array(get_class($subNode->getAttribute(Attribute::ORIGINAL_NODE)), $this->parenthesesMap[$mapKey], true);
}
$addParentheses = $parenthesesNeeded && !$originalTokens->hasParentheses($subStartPos, $subEndPos);
if ($addParentheses) {
$result .= '(';
}
$result .= $this->printNodeFormatPreserving($subNode, $originalTokens);
if ($addParentheses) {
$result .= ')';
}
$pos = $subEndPos + 1;
}
return $result . $originalTokens->getContentBetween($pos, $endPos + 1);
}
}