Composer updates

This commit is contained in:
Clemens Schwaighofer
2023-05-29 16:21:31 +09:00
parent 250067927a
commit 7b5ad92e66
304 changed files with 11398 additions and 3199 deletions

View File

@@ -1,4 +1,4 @@
<h1 align="center">PHPDoc-Parser for PHPStan</h1>
<h1 align="center">PHPDoc Parser for PHPStan</h1>
<p align="center">
<a href="https://github.com/phpstan/phpdoc-parser/actions"><img src="https://github.com/phpstan/phpdoc-parser/workflows/Build/badge.svg" alt="Build Status"></a>
@@ -7,11 +7,99 @@
<a href="https://phpstan.org/"><img src="https://img.shields.io/badge/PHPStan-enabled-brightgreen.svg?style=flat" alt="PHPStan Enabled"></a>
</p>
* [PHPStan](https://phpstan.org)
This library `phpstan/phpdoc-parser` represents PHPDocs with an AST (Abstract Syntax Tree). It supports parsing and modifying PHPDocs.
------
For the complete list of supported PHPDoc features check out PHPStan documentation. PHPStan is the main (but not the only) user of this library.
Next generation phpDoc parser with support for intersection types and generics.
* [PHPDoc Basics](https://phpstan.org/writing-php-code/phpdocs-basics) (list of PHPDoc tags)
* [PHPDoc Types](https://phpstan.org/writing-php-code/phpdoc-types) (list of PHPDoc types)
* [phpdoc-parser API Reference](https://phpstan.github.io/phpdoc-parser/namespace-PHPStan.PhpDocParser.html) with all the AST node types etc.
## Installation
```
composer require phpstan/phpdoc-parser
```
## Basic usage
```php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;
// basic setup
$lexer = new Lexer();
$constExprParser = new ConstExprParser();
$typeParser = new TypeParser($constExprParser);
$phpDocParser = new PhpDocParser($typeParser, $constExprParser);
// parsing and reading a PHPDoc string
$tokens = new TokenIterator($lexer->tokenize('/** @param Lorem $a */'));
$phpDocNode = $phpDocParser->parse($tokens); // PhpDocNode
$paramTags = $phpDocNode->getParamTagValues(); // ParamTagValueNode[]
echo $paramTags[0]->parameterName; // '$a'
echo $paramTags[0]->type; // IdentifierTypeNode - 'Lorem'
```
### Format-preserving printer
This component can be used to modify the AST
and print it again as close as possible to the original.
It's heavily inspired by format-preserving printer component in [nikic/PHP-Parser](https://github.com/nikic/PHP-Parser).
```php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use PHPStan\PhpDocParser\Ast\NodeTraverser;
use PHPStan\PhpDocParser\Ast\NodeVisitor\CloningVisitor;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;
use PHPStan\PhpDocParser\Printer\Printer;
// basic setup with enabled required lexer attributes
$usedAttributes = ['lines' => true, 'indexes' => true];
$lexer = new Lexer();
$constExprParser = new ConstExprParser(true, true, $usedAttributes);
$typeParser = new TypeParser($constExprParser, true, $usedAttributes);
$phpDocParser = new PhpDocParser($typeParser, $constExprParser, true, true, $usedAttributes);
$tokens = new TokenIterator($lexer->tokenize('/** @param Lorem $a */'));
$phpDocNode = $phpDocParser->parse($tokens); // PhpDocNode
$cloningTraverser = new NodeTraverser([new CloningVisitor()]);
/** @var PhpDocNode $newPhpDocNode */
$printer = new Printer();
[$newPhpDocNode] = $cloningTraverser->traverse([$phpDocNode]);
// change something in $newPhpDocNode
$newPhpDocNode->getParamTagValues()[0]->type = new IdentifierTypeNode('Ipsum');
$newPhpDoc = $printer->printFormatPreserving($newPhpDocNode, $phpDocNode, $tokens);
echo $newPhpDoc; // '/** @param Ipsum $a */'
```
## Code of Conduct

View File

@@ -6,6 +6,7 @@
"php": "^7.2 || ^8.0"
},
"require-dev": {
"nikic/php-parser": "^4.15",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^1.5",

View File

@@ -0,0 +1,31 @@
parameters:
ignoreErrors:
-
message: "#^Method PHPStan\\\\PhpDocParser\\\\Ast\\\\ConstExpr\\\\QuoteAwareConstExprStringNode\\:\\:escapeDoubleQuotedString\\(\\) should return string but returns string\\|null\\.$#"
count: 1
path: src/Ast/ConstExpr/QuoteAwareConstExprStringNode.php
-
message: "#^Cannot use array destructuring on array\\<int, array\\<PHPStan\\\\PhpDocParser\\\\Ast\\\\Node\\>\\|int\\|string\\>\\|null\\.$#"
count: 1
path: src/Ast/NodeTraverser.php
-
message: "#^Strict comparison using \\=\\=\\= between 2 and 2 will always evaluate to true\\.$#"
count: 2
path: src/Ast/NodeTraverser.php
-
message: "#^Variable property access on PHPStan\\\\PhpDocParser\\\\Ast\\\\Node\\.$#"
count: 1
path: src/Ast/NodeTraverser.php
-
message: "#^Method PHPStan\\\\PhpDocParser\\\\Parser\\\\StringUnescaper\\:\\:parseEscapeSequences\\(\\) should return string but returns string\\|null\\.$#"
count: 1
path: src/Parser/StringUnescaper.php
-
message: "#^Variable property access on PHPStan\\\\PhpDocParser\\\\Ast\\\\Node\\.$#"
count: 2
path: src/Printer/Printer.php

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
{
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,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,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

@@ -31,10 +31,11 @@ class InvalidTagValueNode implements PhpDocTagValueNode
$exception->getCurrentOffset(),
$exception->getExpectedTokenType(),
$exception->getExpectedTokenValue(),
$exception->getCurrentTokenLine(),
];
}
public function __get(string $name)
public function __get(string $name): ?ParserException
{
if ($name !== 'exception') {
trigger_error(sprintf('Undefined property: %s::$%s', self::class, $name), E_USER_WARNING);

View File

@@ -30,6 +30,10 @@ class MethodTagValueNode implements PhpDocTagValueNode
/** @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;

View File

@@ -23,6 +23,7 @@ class ArrayShapeNode implements TypeNode
public $kind;
/**
* @param ArrayShapeItemNode[] $items
* @param self::KIND_* $kind
*/
public function __construct(array $items, bool $sealed = true, string $kind = self::KIND_ARRAY)

View File

@@ -20,6 +20,14 @@ class ArrayTypeNode implements TypeNode
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

@@ -19,6 +19,9 @@ class CallableTypeNode implements TypeNode
/** @var TypeNode */
public $returnType;
/**
* @param CallableTypeParameterNode[] $parameters
*/
public function __construct(IdentifierTypeNode $identifier, array $parameters, TypeNode $returnType)
{
$this->identifier = $identifier;
@@ -29,8 +32,12 @@ class CallableTypeNode implements TypeNode
public function __toString(): string
{
$returnType = $this->returnType;
if ($returnType instanceof self) {
$returnType = "({$returnType})";
}
$parameters = implode(', ', $this->parameters);
return "{$this->identifier}({$parameters}): {$this->returnType}";
return "{$this->identifier}({$parameters}): {$returnType}";
}
}

View File

@@ -41,8 +41,8 @@ class CallableTypeParameterNode implements Node
$type = "{$this->type} ";
$isReference = $this->isReference ? '&' : '';
$isVariadic = $this->isVariadic ? '...' : '';
$default = $this->isOptional ? ' = default' : '';
return trim("{$type}{$isReference}{$isVariadic}{$this->parameterName}") . $default;
$isOptional = $this->isOptional ? '=' : '';
return trim("{$type}{$isReference}{$isVariadic}{$this->parameterName}") . $isOptional;
}
}

View File

@@ -25,6 +25,10 @@ class GenericTypeNode implements TypeNode
/** @var (self::VARIANCE_*)[] */
public $variances;
/**
* @param TypeNode[] $genericTypes
* @param (self::VARIANCE_*)[] $variances
*/
public function __construct(IdentifierTypeNode $type, array $genericTypes, array $variances = [])
{
$this->type = $type;

View File

@@ -3,6 +3,7 @@
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function array_map;
use function implode;
class IntersectionTypeNode implements TypeNode
@@ -13,6 +14,9 @@ class IntersectionTypeNode implements TypeNode
/** @var TypeNode[] */
public $types;
/**
* @param TypeNode[] $types
*/
public function __construct(array $types)
{
$this->types = $types;
@@ -21,7 +25,13 @@ class IntersectionTypeNode implements TypeNode
public function __toString(): string
{
return '(' . implode(' & ', $this->types) . ')';
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,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

@@ -23,6 +23,14 @@ class OffsetAccessTypeNode implements TypeNode
public function __toString(): string
{
if (
$this->type instanceof CallableTypeNode
|| $this->type instanceof ConstTypeNode
|| $this->type instanceof NullableTypeNode
) {
return '(' . $this->type . ')[' . $this->offset . ']';
}
return $this->type . '[' . $this->offset . ']';
}

View File

@@ -3,6 +3,7 @@
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function array_map;
use function implode;
class UnionTypeNode implements TypeNode
@@ -13,6 +14,9 @@ class UnionTypeNode implements TypeNode
/** @var TypeNode[] */
public $types;
/**
* @param TypeNode[] $types
*/
public function __construct(array $types)
{
$this->types = $types;
@@ -21,7 +25,13 @@ class UnionTypeNode implements TypeNode
public function __toString(): string
{
return '(' . implode(' | ', $this->types) . ')';
return '(' . implode(' | ', array_map(static function (TypeNode $type): string {
if ($type instanceof NullableTypeNode) {
return '(' . $type . ')';
}
return (string) $type;
}, $this->types)) . ')';
}
}

View File

@@ -88,10 +88,14 @@ class Lexer
public const VALUE_OFFSET = 0;
public const TYPE_OFFSET = 1;
public const LINE_OFFSET = 2;
/** @var string|null */
private $regexp;
/**
* @return list<array{string, int, int}>
*/
public function tokenize(string $s): array
{
if ($this->regexp === null) {
@@ -101,11 +105,18 @@ class Lexer
preg_match_all($this->regexp, $s, $matches, PREG_SET_ORDER);
$tokens = [];
$line = 1;
foreach ($matches as $match) {
$tokens[] = [$match[0], (int) $match['MARK']];
$type = (int) $match['MARK'];
$tokens[] = [$match[0], $type, $line];
if ($type !== self::TOKEN_PHPDOC_EOL) {
continue;
}
$line++;
}
$tokens[] = ['', self::TOKEN_END];
$tokens[] = ['', self::TOKEN_END, $line];
return $tokens;
}
@@ -149,8 +160,8 @@ class Lexer
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]*+(?:e-?[0-9]++)?)|(?:-?[0-9]*+\\.[0-9]++(?:e-?[0-9]++)?)|(?:-?[0-9]++e-?[0-9]++)',
self::TOKEN_INTEGER => '-?(?:(?:0b[0-1]++)|(?:0o[0-7]++)|(?:0x[0-9a-f]++)|(?:[0-9]++))',
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\\\\])*+"',

View File

@@ -4,10 +4,6 @@ namespace PHPStan\PhpDocParser\Parser;
use PHPStan\PhpDocParser\Ast;
use PHPStan\PhpDocParser\Lexer\Lexer;
use function chr;
use function hexdec;
use function octdec;
use function preg_replace_callback;
use function str_replace;
use function strtolower;
use function substr;
@@ -15,49 +11,93 @@ use function substr;
class ConstExprParser
{
private const REPLACEMENTS = [
'\\' => '\\',
'n' => "\n",
'r' => "\r",
't' => "\t",
'f' => "\f",
'v' => "\v",
'e' => "\x1B",
];
/** @var bool */
private $unescapeStrings;
public function __construct(bool $unescapeStrings = false)
/** @var bool */
private $quoteAwareConstExprString;
/** @var bool */
private $useLinesAttributes;
/** @var bool */
private $useIndexAttributes;
/**
* @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;
}
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 new Ast\ConstExpr\ConstExprFloatNode($value);
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 new Ast\ConstExpr\ConstExprIntegerNode($value);
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $value)),
$startLine,
$startIndex
);
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING, Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
$value = $tokens->currentTokenValue();
$type = $tokens->currentTokenType();
if ($trimStrings) {
if ($this->unescapeStrings) {
$value = self::unescapeString($value);
$value = StringUnescaper::unescapeString($value);
} else {
$value = substr($value, 1, -1);
}
}
$tokens->next();
return new Ast\ConstExpr\ConstExprStringNode($value);
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();
@@ -65,14 +105,29 @@ class ConstExprParser
switch (strtolower($identifier)) {
case 'true':
return new Ast\ConstExpr\ConstExprTrueNode();
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprTrueNode(),
$startLine,
$startIndex
);
case 'false':
return new Ast\ConstExpr\ConstExprFalseNode();
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprFalseNode(),
$startLine,
$startIndex
);
case 'null':
return new Ast\ConstExpr\ConstExprNullNode();
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);
return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_PARENTHESES, $startIndex);
}
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
@@ -106,29 +161,43 @@ class ConstExprParser
break;
}
return new Ast\ConstExpr\ConstFetchNode($identifier, $classConstantName);
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstFetchNode($identifier, $classConstantName),
$startLine,
$startIndex
);
}
return new Ast\ConstExpr\ConstFetchNode('', $identifier);
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);
return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_SQUARE_BRACKET, $startIndex);
}
throw new ParserException(
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Lexer::TOKEN_IDENTIFIER
Lexer::TOKEN_IDENTIFIER,
null,
$tokens->currentTokenLine()
);
}
private function parseArray(TokenIterator $tokens, int $endToken): Ast\ConstExpr\ConstExprArrayNode
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);
@@ -136,12 +205,20 @@ class ConstExprParser
$tokens->consumeTokenType($endToken);
}
return new Ast\ConstExpr\ConstExprArrayNode($items);
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprArrayNode($items),
$startLine,
$startIndex
);
}
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)) {
@@ -153,78 +230,40 @@ class ConstExprParser
$value = $expr;
}
return new Ast\ConstExpr\ConstExprArrayItemNode($key, $value);
}
private 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(hexdec(substr($str, 1)));
}
if ($str[0] === 'u') {
return self::codePointToUtf8(hexdec($matches[2]));
}
return chr(octdec($str));
},
$str
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprArrayItemNode($key, $value),
$startLine,
$startIndex
);
}
/**
* Implementation based on https://github.com/nikic/PHP-Parser/blob/b0edd4c41111042d43bb45c6c657b2e0db367d9e/lib/PhpParser/Node/Scalar/String_.php#L132-L154
* @template T of Ast\ConstExpr\ConstExprNode
* @param T $node
* @return T
*/
private static function codePointToUtf8(int $num): string
private function enrichWithAttributes(TokenIterator $tokens, Ast\ConstExpr\ConstExprNode $node, int $startLine, int $startIndex): Ast\ConstExpr\ConstExprNode
{
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);
$endLine = $tokens->currentTokenLine();
$endIndex = $tokens->currentTokenIndex();
if ($this->useLinesAttributes) {
$node->setAttribute(Ast\Attribute::START_LINE, $startLine);
$node->setAttribute(Ast\Attribute::END_LINE, $endLine);
}
// Invalid UTF-8 codepoint escape sequence: Codepoint too large
return "\xef\xbf\xbd";
if ($this->useIndexAttributes) {
$tokensArray = $tokens->getTokens();
$endIndex--;
if ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) {
$endIndex--;
}
$node->setAttribute(Ast\Attribute::START_INDEX, $startIndex);
$node->setAttribute(Ast\Attribute::END_INDEX, $endIndex);
}
return $node;
}
}

View File

@@ -7,6 +7,7 @@ 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;
@@ -28,12 +29,16 @@ class ParserException extends Exception
/** @var string|null */
private $expectedTokenValue;
/** @var int|null */
private $currentTokenLine;
public function __construct(
string $currentTokenValue,
int $currentTokenType,
int $currentOffset,
int $expectedTokenType,
?string $expectedTokenValue = null
?string $expectedTokenValue = null,
?int $currentTokenLine = null
)
{
$this->currentTokenValue = $currentTokenValue;
@@ -41,13 +46,15 @@ class ParserException extends Exception
$this->currentOffset = $currentOffset;
$this->expectedTokenType = $expectedTokenType;
$this->expectedTokenValue = $expectedTokenValue;
$this->currentTokenLine = $currentTokenLine;
parent::__construct(sprintf(
'Unexpected token %s, expected %s%s at offset %d',
'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
$currentOffset,
$currentTokenLine === null ? '' : sprintf(' on line %d', $currentTokenLine)
));
}
@@ -82,9 +89,15 @@ class ParserException extends Exception
}
public function getCurrentTokenLine(): ?int
{
return $this->currentTokenLine;
}
private function formatValue(string $value): string
{
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
assert($json !== false);
return $json;

View File

@@ -28,11 +28,32 @@ class PhpDocParser
/** @var bool */
private $requireWhitespaceBeforeDescription;
public function __construct(TypeParser $typeParser, ConstExprParser $constantExprParser, bool $requireWhitespaceBeforeDescription = false)
/** @var bool */
private $preserveTypeAliasesWithInvalidTypes;
/** @var bool */
private $useLinesAttributes;
/** @var bool */
private $useIndexAttributes;
/**
* @param array{lines?: bool, indexes?: bool} $usedAttributes
*/
public function __construct(
TypeParser $typeParser,
ConstExprParser $constantExprParser,
bool $requireWhitespaceBeforeDescription = false,
bool $preserveTypeAliasesWithInvalidTypes = false,
array $usedAttributes = []
)
{
$this->typeParser = $typeParser;
$this->constantExprParser = $constantExprParser;
$this->requireWhitespaceBeforeDescription = $requireWhitespaceBeforeDescription;
$this->preserveTypeAliasesWithInvalidTypes = $preserveTypeAliasesWithInvalidTypes;
$this->useLinesAttributes = $usedAttributes['lines'] ?? false;
$this->useIndexAttributes = $usedAttributes['indexes'] ?? false;
}
@@ -54,30 +75,82 @@ class PhpDocParser
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PHPDOC);
} catch (ParserException $e) {
$name = '';
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
if (count($children) > 0) {
$lastChild = $children[count($children) - 1];
if ($lastChild instanceof Ast\PhpDoc\PhpDocTagNode) {
$name = $lastChild->name;
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
}
}
$tag = new Ast\PhpDoc\PhpDocTagNode(
$name,
$this->enrichWithAttributes(
$tokens,
new Ast\PhpDoc\InvalidTagValueNode($e->getMessage(), $e),
$startLine,
$startIndex
)
);
$tokens->forwardToTheEnd();
return new Ast\PhpDoc\PhpDocNode([
new Ast\PhpDoc\PhpDocTagNode($name, new Ast\PhpDoc\InvalidTagValueNode($e->getMessage(), $e)),
]);
return $this->enrichWithAttributes($tokens, new Ast\PhpDoc\PhpDocNode([$this->enrichWithAttributes($tokens, $tag, $startLine, $startIndex)]), 1, 0);
}
return new Ast\PhpDoc\PhpDocNode(array_values($children));
return $this->enrichWithAttributes($tokens, new Ast\PhpDoc\PhpDocNode(array_values($children)), 1, 0);
}
private function parseChild(TokenIterator $tokens): Ast\PhpDoc\PhpDocChildNode
{
if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG)) {
return $this->parseTag($tokens);
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
return $this->enrichWithAttributes($tokens, $this->parseTag($tokens), $startLine, $startIndex);
}
return $this->parseText($tokens);
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
$text = $this->parseText($tokens);
return $this->enrichWithAttributes($tokens, $text, $startLine, $startIndex);
}
/**
* @template T of Ast\Node
* @param T $tag
* @return T
*/
private function enrichWithAttributes(TokenIterator $tokens, Ast\Node $tag, int $startLine, int $startIndex): Ast\Node
{
$endLine = $tokens->currentTokenLine();
$endIndex = $tokens->currentTokenIndex();
if ($this->useLinesAttributes) {
$tag->setAttribute(Ast\Attribute::START_LINE, $startLine);
$tag->setAttribute(Ast\Attribute::END_LINE, $endLine);
}
if ($this->useIndexAttributes) {
$tokensArray = $tokens->getTokens();
if ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_CLOSE_PHPDOC) {
$endIndex--;
if ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) {
$endIndex--;
}
} elseif ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_PHPDOC_EOL) {
$endIndex--;
}
$tag->setAttribute(Ast\Attribute::START_INDEX, $startIndex);
$tag->setAttribute(Ast\Attribute::END_INDEX, $endIndex);
}
return $tag;
}
@@ -120,6 +193,9 @@ class PhpDocParser
public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\PhpDocTagValueNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
try {
$tokens->pushSavePoint();
@@ -247,7 +323,7 @@ class PhpDocParser
$tagValue = new Ast\PhpDoc\InvalidTagValueNode($this->parseOptionalDescription($tokens), $e);
}
return $tagValue;
return $this->enrichWithAttributes($tokens, $tagValue, $startLine, $startIndex);
}
@@ -257,9 +333,7 @@ class PhpDocParser
private function parseParamTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
{
if (
$tokens->isCurrentTokenType(Lexer::TOKEN_REFERENCE)
|| $tokens->isCurrentTokenType(Lexer::TOKEN_VARIADIC)
|| $tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)
$tokens->isCurrentTokenType(Lexer::TOKEN_REFERENCE, Lexer::TOKEN_VARIADIC, Lexer::TOKEN_VARIABLE)
) {
$type = null;
} else {
@@ -329,6 +403,8 @@ class PhpDocParser
private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueNode
{
$isStatic = $tokens->tryConsumeTokenValue('static');
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
$returnTypeOrMethodName = $this->typeParser->parse($tokens);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
@@ -337,7 +413,9 @@ class PhpDocParser
$tokens->next();
} elseif ($returnTypeOrMethodName instanceof Ast\Type\IdentifierTypeNode) {
$returnType = $isStatic ? new Ast\Type\IdentifierTypeNode('static') : null;
$returnType = $isStatic
? $this->typeParser->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode('static'), $startLine, $startIndex)
: null;
$methodName = $returnTypeOrMethodName->name;
$isStatic = false;
@@ -347,9 +425,12 @@ class PhpDocParser
}
$templateTypes = [];
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
do {
$templateTypes[] = $this->parseTemplateTagValue($tokens, false);
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
$templateTypes[] = $this->enrichWithAttributes($tokens, $this->parseTemplateTagValue($tokens, false), $startLine, $startIndex);
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
}
@@ -370,6 +451,9 @@ class PhpDocParser
private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueParameterNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
switch ($tokens->currentTokenType()) {
case Lexer::TOKEN_IDENTIFIER:
case Lexer::TOKEN_OPEN_PARENTHESES:
@@ -394,7 +478,12 @@ class PhpDocParser
$defaultValue = null;
}
return new Ast\PhpDoc\MethodTagValueParameterNode($parameterType, $isReference, $isVariadic, $parameterName, $defaultValue);
return $this->enrichWithAttributes(
$tokens,
new Ast\PhpDoc\MethodTagValueParameterNode($parameterType, $isReference, $isVariadic, $parameterName, $defaultValue),
$startLine,
$startIndex
);
}
private function parseTemplateTagValue(TokenIterator $tokens, bool $parseDescription): Ast\PhpDoc\TemplateTagValueNode
@@ -426,10 +515,15 @@ class PhpDocParser
private function parseExtendsTagValue(string $tagName, TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
$baseType = new IdentifierTypeNode($tokens->currentTokenValue());
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
$type = $this->typeParser->parseGeneric($tokens, $baseType);
$type = $this->typeParser->parseGeneric(
$tokens,
$this->typeParser->enrichWithAttributes($tokens, $baseType, $startLine, $startIndex)
);
$description = $this->parseOptionalDescription($tokens);
@@ -453,6 +547,34 @@ class PhpDocParser
// support psalm-type syntax
$tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
if ($this->preserveTypeAliasesWithInvalidTypes) {
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
try {
$type = $this->typeParser->parse($tokens);
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
throw new ParserException(
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Lexer::TOKEN_PHPDOC_EOL,
null,
$tokens->currentTokenLine()
);
}
}
return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type);
} catch (ParserException $e) {
$this->parseOptionalDescription($tokens);
return new Ast\PhpDoc\TypeAliasTagValueNode(
$alias,
$this->enrichWithAttributes($tokens, new Ast\Type\InvalidTypeNode($e), $startLine, $startIndex)
);
}
}
$type = $this->typeParser->parse($tokens);
return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type);
@@ -465,8 +587,16 @@ class PhpDocParser
$tokens->consumeTokenValue(Lexer::TOKEN_IDENTIFIER, 'from');
$identifierStartLine = $tokens->currentTokenLine();
$identifierStartIndex = $tokens->currentTokenIndex();
$importedFrom = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
$importedFromType = $this->enrichWithAttributes(
$tokens,
new IdentifierTypeNode($importedFrom),
$identifierStartLine,
$identifierStartIndex
);
$importedAs = null;
if ($tokens->tryConsumeTokenValue('as')) {
@@ -474,7 +604,7 @@ class PhpDocParser
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
}
return new Ast\PhpDoc\TypeAliasImportTagValueNode($importedAlias, new IdentifierTypeNode($importedFrom), $importedAs);
return new Ast\PhpDoc\TypeAliasImportTagValueNode($importedAlias, $importedFromType, $importedAs);
}
/**

View File

@@ -0,0 +1,96 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Parser;
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') {
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

@@ -2,6 +2,7 @@
namespace PHPStan\PhpDocParser\Parser;
use LogicException;
use PHPStan\PhpDocParser\Lexer\Lexer;
use function array_pop;
use function assert;
@@ -12,7 +13,7 @@ use function strlen;
class TokenIterator
{
/** @var mixed[][] */
/** @var list<array{string, int, int}> */
private $tokens;
/** @var int */
@@ -21,6 +22,9 @@ class TokenIterator
/** @var int[] */
private $savePoints = [];
/**
* @param list<array{string, int, int}> $tokens
*/
public function __construct(array $tokens, int $index = 0)
{
$this->tokens = $tokens;
@@ -34,6 +38,36 @@ class TokenIterator
}
/**
* @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];
@@ -57,6 +91,18 @@ class TokenIterator
}
public function currentTokenLine(): int
{
return $this->tokens[$this->index][Lexer::LINE_OFFSET];
}
public function currentTokenIndex(): int
{
return $this->index;
}
public function isCurrentTokenValue(string $tokenValue): bool
{
return $this->tokens[$this->index][Lexer::VALUE_OFFSET] === $tokenValue;
@@ -217,8 +263,69 @@ class TokenIterator
$this->currentTokenType(),
$this->currentTokenOffset(),
$expectedTokenType,
$expectedTokenValue
$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;
}
/**
* 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);
}
}

View File

@@ -6,6 +6,7 @@ use LogicException;
use PHPStan\PhpDocParser\Ast;
use PHPStan\PhpDocParser\Lexer\Lexer;
use function in_array;
use function str_replace;
use function strpos;
use function trim;
@@ -15,14 +16,35 @@ class TypeParser
/** @var ConstExprParser|null */
private $constExprParser;
public function __construct(?ConstExprParser $constExprParser = null)
/** @var bool */
private $quoteAwareConstExprString;
/** @var bool */
private $useLinesAttributes;
/** @var bool */
private $useIndexAttributes;
/**
* @param array{lines?: bool, indexes?: bool} $usedAttributes
*/
public function __construct(
?ConstExprParser $constExprParser = null,
bool $quoteAwareConstExprString = false,
array $usedAttributes = []
)
{
$this->constExprParser = $constExprParser;
$this->quoteAwareConstExprString = $quoteAwareConstExprString;
$this->useLinesAttributes = $usedAttributes['lines'] ?? false;
$this->useIndexAttributes = $usedAttributes['indexes'] ?? false;
}
/** @phpstan-impure */
public function parse(TokenIterator $tokens): Ast\Type\TypeNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
$type = $this->parseNullable($tokens);
@@ -37,12 +59,45 @@ class TypeParser
}
}
return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
}
/**
* @internal
* @template T of Ast\Node
* @param T $type
* @return T
*/
public function enrichWithAttributes(TokenIterator $tokens, Ast\Node $type, int $startLine, int $startIndex): Ast\Node
{
$endLine = $tokens->currentTokenLine();
$endIndex = $tokens->currentTokenIndex();
if ($this->useLinesAttributes) {
$type->setAttribute(Ast\Attribute::START_LINE, $startLine);
$type->setAttribute(Ast\Attribute::END_LINE, $endLine);
}
if ($this->useIndexAttributes) {
$tokensArray = $tokens->getTokens();
$endIndex--;
if ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) {
$endIndex--;
}
$type->setAttribute(Ast\Attribute::START_INDEX, $startIndex);
$type->setAttribute(Ast\Attribute::END_INDEX, $endIndex);
}
return $type;
}
/** @phpstan-impure */
private function subParse(TokenIterator $tokens): Ast\Type\TypeNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
$type = $this->parseNullable($tokens);
@@ -66,13 +121,16 @@ class TypeParser
}
}
return $type;
return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
}
/** @phpstan-impure */
private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$type = $this->subParse($tokens);
@@ -81,26 +139,26 @@ class TypeParser
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
return $this->tryParseArrayOrOffsetAccess($tokens, $type);
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
}
return $type;
return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
}
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
$type = new Ast\Type\ThisTypeNode();
$type = $this->enrichWithAttributes($tokens, new Ast\Type\ThisTypeNode(), $startLine, $startIndex);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
return $this->tryParseArrayOrOffsetAccess($tokens, $type);
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
}
return $type;
return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
}
$currentTokenValue = $tokens->currentTokenValue();
$tokens->pushSavePoint(); // because of ConstFetchNode
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
$type = new Ast\Type\IdentifierTypeNode($currentTokenValue);
$type = $this->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode($currentTokenValue), $startLine, $startIndex);
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
$tokens->dropSavePoint(); // because of ConstFetchNode
@@ -124,15 +182,22 @@ class TypeParser
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
} elseif (in_array($type->name, ['array', 'list'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) {
$type = $this->parseArrayShape($tokens, $type, $type->name);
} elseif (in_array($type->name, ['array', 'list', 'object'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) {
if ($type->name === 'object') {
$type = $this->parseObjectShape($tokens);
} else {
$type = $this->parseArrayShape($tokens, $type, $type->name);
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
$type = $this->tryParseArrayOrOffsetAccess(
$tokens,
$this->enrichWithAttributes($tokens, $type, $startLine, $startIndex)
);
}
}
return $type;
return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
} else {
$tokens->rollback(); // because of ConstFetchNode
}
@@ -144,7 +209,9 @@ class TypeParser
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Lexer::TOKEN_IDENTIFIER
Lexer::TOKEN_IDENTIFIER,
null,
$tokens->currentTokenLine()
);
if ($this->constExprParser === null) {
@@ -157,7 +224,7 @@ class TypeParser
throw $exception;
}
return new Ast\Type\ConstTypeNode($constExpr);
return $this->enrichWithAttributes($tokens, new Ast\Type\ConstTypeNode($constExpr), $startLine, $startIndex);
} catch (LogicException $e) {
throw $exception;
}
@@ -336,7 +403,14 @@ class TypeParser
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
// trailing comma case
return new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances);
$type = new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances);
$startLine = $baseType->getAttribute(Ast\Attribute::START_LINE);
$startIndex = $baseType->getAttribute(Ast\Attribute::START_INDEX);
if ($startLine !== null && $startIndex !== null) {
$type = $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
}
return $type;
}
[$genericTypes[], $variances[]] = $this->parseGenericTypeArgument($tokens);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
@@ -345,7 +419,14 @@ class TypeParser
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
return new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances);
$type = new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances);
$startLine = $baseType->getAttribute(Ast\Attribute::START_LINE);
$startIndex = $baseType->getAttribute(Ast\Attribute::START_INDEX);
if ($startLine !== null && $startIndex !== null) {
$type = $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
}
return $type;
}
@@ -355,9 +436,11 @@ class TypeParser
*/
public function parseGenericTypeArgument(TokenIterator $tokens): array
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) {
return [
new Ast\Type\IdentifierTypeNode('mixed'),
$this->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode('mixed'), $startLine, $startIndex),
Ast\Type\GenericTypeNode::VARIANCE_BIVARIANT,
];
}
@@ -397,7 +480,10 @@ class TypeParser
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
$returnType = $this->parseCallableReturnType($tokens);
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
$returnType = $this->enrichWithAttributes($tokens, $this->parseCallableReturnType($tokens), $startLine, $startIndex);
return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType);
}
@@ -406,6 +492,8 @@ class TypeParser
/** @phpstan-impure */
private function parseCallableParameter(TokenIterator $tokens): Ast\Type\CallableTypeParameterNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
$type = $this->parse($tokens);
$isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE);
$isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC);
@@ -419,37 +507,141 @@ class TypeParser
}
$isOptional = $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
return new Ast\Type\CallableTypeParameterNode($type, $isReference, $isVariadic, $parameterName, $isOptional);
return $this->enrichWithAttributes(
$tokens,
new Ast\Type\CallableTypeParameterNode($type, $isReference, $isVariadic, $parameterName, $isOptional),
$startLine,
$startIndex
);
}
/** @phpstan-impure */
private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
$type = $this->parseNullable($tokens);
return $this->parseNullable($tokens);
} elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
$type = $this->parse($tokens);
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
}
return $type;
} elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
$type = new Ast\Type\ThisTypeNode();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes(
$tokens,
$type,
$startLine,
$startIndex
));
}
return $type;
} else {
$type = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue());
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
$currentTokenValue = $tokens->currentTokenValue();
$tokens->pushSavePoint(); // because of ConstFetchNode
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
$type = new Ast\Type\IdentifierTypeNode($currentTokenValue);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
$type = $this->parseGeneric($tokens, $type);
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
$type = $this->parseGeneric(
$tokens,
$this->enrichWithAttributes(
$tokens,
$type,
$startLine,
$startIndex
)
);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes(
$tokens,
$type,
$startLine,
$startIndex
));
}
} elseif (in_array($type->name, ['array', 'list'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) {
$type = $this->parseArrayShape($tokens, $type, $type->name);
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes(
$tokens,
$type,
$startLine,
$startIndex
));
} elseif (in_array($type->name, ['array', 'list', 'object'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) {
if ($type->name === 'object') {
$type = $this->parseObjectShape($tokens);
} else {
$type = $this->parseArrayShape($tokens, $this->enrichWithAttributes(
$tokens,
$type,
$startLine,
$startIndex
), $type->name);
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes(
$tokens,
$type,
$startLine,
$startIndex
));
}
}
return $type;
} else {
$tokens->rollback(); // because of ConstFetchNode
}
} else {
$tokens->dropSavePoint(); // because of ConstFetchNode
}
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
$exception = new ParserException(
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Lexer::TOKEN_IDENTIFIER,
null,
$tokens->currentTokenLine()
);
if ($this->constExprParser === null) {
throw $exception;
}
return $type;
try {
$constExpr = $this->constExprParser->parse($tokens, true);
if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) {
throw $exception;
}
$type = new Ast\Type\ConstTypeNode($constExpr);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes(
$tokens,
$type,
$startLine,
$startIndex
));
}
return $type;
} catch (LogicException $e) {
throw $exception;
}
}
@@ -473,6 +665,8 @@ class TypeParser
/** @phpstan-impure */
private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
{
$startLine = $type->getAttribute(Ast\Attribute::START_LINE);
$startIndex = $type->getAttribute(Ast\Attribute::START_INDEX);
try {
while ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$tokens->pushSavePoint();
@@ -485,10 +679,28 @@ class TypeParser
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET);
$tokens->dropSavePoint();
$type = new Ast\Type\OffsetAccessTypeNode($type, $offset);
if ($startLine !== null && $startIndex !== null) {
$type = $this->enrichWithAttributes(
$tokens,
$type,
$startLine,
$startIndex
);
}
} else {
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET);
$tokens->dropSavePoint();
$type = new Ast\Type\ArrayTypeNode($type);
if ($startLine !== null && $startIndex !== null) {
$type = $this->enrichWithAttributes(
$tokens,
$type,
$startLine,
$startIndex
);
}
}
}
@@ -539,6 +751,8 @@ class TypeParser
/** @phpstan-impure */
private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShapeItemNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
try {
$tokens->pushSavePoint();
$key = $this->parseArrayShapeKey($tokens);
@@ -547,12 +761,22 @@ class TypeParser
$value = $this->parse($tokens);
$tokens->dropSavePoint();
return new Ast\Type\ArrayShapeItemNode($key, $optional, $value);
return $this->enrichWithAttributes(
$tokens,
new Ast\Type\ArrayShapeItemNode($key, $optional, $value),
$startLine,
$startIndex
);
} catch (ParserException $e) {
$tokens->rollback();
$value = $this->parse($tokens);
return new Ast\Type\ArrayShapeItemNode(null, false, $value);
return $this->enrichWithAttributes(
$tokens,
new Ast\Type\ArrayShapeItemNode(null, false, $value),
$startLine,
$startIndex
);
}
}
@@ -562,16 +786,28 @@ class TypeParser
*/
private function parseArrayShapeKey(TokenIterator $tokens)
{
$startIndex = $tokens->currentTokenIndex();
$startLine = $tokens->currentTokenLine();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
$key = new Ast\ConstExpr\ConstExprIntegerNode($tokens->currentTokenValue());
$key = new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $tokens->currentTokenValue()));
$tokens->next();
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'"));
if ($this->quoteAwareConstExprString) {
$key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED);
} else {
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'"));
}
$tokens->next();
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"'));
if ($this->quoteAwareConstExprString) {
$key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED);
} else {
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"'));
}
$tokens->next();
} else {
@@ -579,7 +815,86 @@ class TypeParser
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
}
return $key;
return $this->enrichWithAttributes(
$tokens,
$key,
$startLine,
$startIndex
);
}
/**
* @phpstan-impure
*/
private function parseObjectShape(TokenIterator $tokens): Ast\Type\ObjectShapeNode
{
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET);
$items = [];
do {
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
return new Ast\Type\ObjectShapeNode($items);
}
$items[] = $this->parseObjectShapeItem($tokens);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);
return new Ast\Type\ObjectShapeNode($items);
}
/** @phpstan-impure */
private function parseObjectShapeItem(TokenIterator $tokens): Ast\Type\ObjectShapeItemNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
$key = $this->parseObjectShapeKey($tokens);
$optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE);
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
$value = $this->parse($tokens);
return $this->enrichWithAttributes($tokens, new Ast\Type\ObjectShapeItemNode($key, $optional, $value), $startLine, $startIndex);
}
/**
* @phpstan-impure
* @return Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode
*/
private function parseObjectShapeKey(TokenIterator $tokens)
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
if ($this->quoteAwareConstExprString) {
$key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED);
} else {
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'"));
}
$tokens->next();
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
if ($this->quoteAwareConstExprString) {
$key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED);
} else {
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"'));
}
$tokens->next();
} else {
$key = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue());
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
}
return $this->enrichWithAttributes($tokens, $key, $startLine, $startIndex);
}
}

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,796 @@
<?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\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\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\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\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' => ', ',
GenericTypeNode::class . '->genericTypes' => ', ',
ConstExprArrayNode::class . '->items' => ', ',
MethodTagValueNode::class . '->parameters' => ', ',
];
/**
* [$find, $extraLeft, $extraRight]
*
* @var array<string, array{string|null, string, string}>
*/
private $emptyListInsertionMap = [
CallableTypeNode::class . '->parameters' => ['(', '', ''],
ArrayShapeNode::class . '->items' => ['{', '', ''],
ObjectShapeNode::class . '->items' => ['{', '', ''],
];
/** @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,
ConstTypeNode::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) {
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;
}
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 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 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) {
$bound = $node->bound !== null ? ' of ' . $this->printType($node->bound) : '';
$default = $node->default !== null ? ' = ' . $this->printType($node->default) : '';
return trim("{$node->name}{$bound}{$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[] = '...';
}
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);
}
$parameters = implode(', ', array_map(function (CallableTypeParameterNode $parameterNode): string {
return $this->print($parameterNode);
}, $node->parameters));
return "{$node->identifier}({$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 ConstTypeNode
|| $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("\n%s*%s", $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("\n%s*%s", $beforeAsteriskIndent, $afterAsteriskIndent);
} else {
$result .= $insertStr;
}
}
$delayedAdd = [];
}
$parenthesesNeeded = isset($this->parenthesesListMap[$mapKey])
&& in_array(get_class($newNode), $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("\n%s*%s", $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("\n%s*%s", $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 ($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);
$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);
}
}