🆙 Add cms i using 🆙

This commit is contained in:
Remco
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2016 Ondřej Mirtes
Copyright (c) 2025 PHPStan s.r.o.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,123 @@
<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>
<a href="https://packagist.org/packages/phpstan/phpdoc-parser"><img src="https://poser.pugx.org/phpstan/phpdoc-parser/v/stable" alt="Latest Stable Version"></a>
<a href="https://choosealicense.com/licenses/mit/"><img src="https://poser.pugx.org/phpstan/phpstan/license" alt="License"></a>
<a href="https://phpstan.org/"><img src="https://img.shields.io/badge/PHPStan-enabled-brightgreen.svg?style=flat" alt="PHPStan Enabled"></a>
</p>
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.
* [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/2.3.x/namespace-PHPStan.PhpDocParser.html) with all the AST node types etc.
This parser also supports parsing [Doctrine Annotations](https://github.com/doctrine/annotations). The AST nodes live in the [PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine namespace](https://phpstan.github.io/phpdoc-parser/2.1.x/namespace-PHPStan.PhpDocParser.Ast.PhpDoc.Doctrine.html).
## 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\ParserConfig;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;
// basic setup
$config = new ParserConfig(usedAttributes: []);
$lexer = new Lexer($config);
$constExprParser = new ConstExprParser($config);
$typeParser = new TypeParser($config, $constExprParser);
$phpDocParser = new PhpDocParser($config, $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\ParserConfig;
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
$config = new ParserConfig(usedAttributes: ['lines' => true, 'indexes' => true, 'comments' => true]);
$lexer = new Lexer($config);
$constExprParser = new ConstExprParser($config);
$typeParser = new TypeParser($config, $constExprParser);
$phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser);
$tokens = new TokenIterator($lexer->tokenize('/** @param Lorem $a */'));
$phpDocNode = $phpDocParser->parse($tokens); // PhpDocNode
$cloningTraverser = new NodeTraverser([new CloningVisitor()]);
/** @var PhpDocNode $newPhpDocNode */
[$newPhpDocNode] = $cloningTraverser->traverse([$phpDocNode]);
// change something in $newPhpDocNode
$newPhpDocNode->getParamTagValues()[0]->type = new IdentifierTypeNode('Ipsum');
// print changed PHPDoc
$printer = new Printer();
$newPhpDoc = $printer->printFormatPreserving($newPhpDocNode, $phpDocNode, $tokens);
echo $newPhpDoc; // '/** @param Ipsum $a */'
```
## Code of Conduct
This project adheres to a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project and its community, you are expected to uphold this code.
## Building
Initially you need to run `composer install`, or `composer update` in case you aren't working in a folder which was built before.
Afterwards you can either run the whole build including linting and coding standards using
make
or run only tests using
make tests
@@ -0,0 +1,129 @@
Upgrading from phpstan/phpdoc-parser 1.x to 2.0
=================================
### PHP version requirements
phpstan/phpdoc-parser now requires PHP 7.4 or newer to run.
### Changed constructors of parser classes
Instead of different arrays and boolean values passed into class constructors during setup, parser classes now share a common ParserConfig object.
Before:
```php
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\TypeParser;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
$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);
```
After:
```php
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\ParserConfig;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\TypeParser;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
$config = new ParserConfig(usedAttributes: ['lines' => true, 'indexes' => true]);
$lexer = new Lexer($config);
$constExprParser = new ConstExprParser($config);
$typeParser = new TypeParser($config, $constExprParser);
$phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser);
```
The point of ParserConfig is that over the course of phpstan/phpdoc-parser 2.x development series it's most likely going to gain new optional parameters akin to PHPStan's [bleeding edge](https://phpstan.org/blog/what-is-bleeding-edge). These parameters will allow opting in to new behaviour which will become the default in 3.0.
With ParserConfig object, it's now going to be impossible to configure parser classes inconsistently. Which [happened to users](https://github.com/phpstan/phpdoc-parser/issues/251#issuecomment-2333927959) when they were separate boolean values.
### Support for parsing Doctrine annotations
This parser now supports parsing [Doctrine Annotations](https://github.com/doctrine/annotations). The AST nodes representing Doctrine Annotations live in the [PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine namespace](https://phpstan.github.io/phpdoc-parser/2.0.x/namespace-PHPStan.PhpDocParser.Ast.PhpDoc.Doctrine.html).
### Whitespace before description is required
phpdoc-parser 1.x sometimes silently consumed invalid part of a PHPDoc type as description:
```php
/** @return \Closure(...int, string): string */
```
This became `IdentifierTypeNode` of `\Closure` and with `(...int, string): string` as description. (Valid callable syntax is: `\Closure(int ...$u, string): string`.)
Another example:
```php
/** @return array{foo: int}} */
```
The extra `}` also became description.
Both of these examples are now InvalidTagValueNode.
If these parts are supposed to be PHPDoc descriptions, you need to put whitespace between the type and the description text:
```php
/** @return \Closure (...int, string): string */
/** @return array{foo: int} } */
```
### Type aliases with invalid types are preserved
In phpdoc-parser 1.x, invalid type alias syntax was represented as [`InvalidTagValueNode`](https://phpstan.github.io/phpdoc-parser/2.0.x/PHPStan.PhpDocParser.Ast.PhpDoc.InvalidTagValueNode.html), losing information about a type alias being present.
```php
/**
* @phpstan-type TypeAlias
*/
```
This `@phpstan-type` is missing the actual type to alias. In phpdoc-parser 2.0 this is now represented as [`TypeAliasTagValueNode`](https://phpstan.github.io/phpdoc-parser/2.0.x/PHPStan.PhpDocParser.Ast.PhpDoc.TypeAliasTagValueNode.html) (instead of `InvalidTagValueNode`) with [`InvalidTypeNode`](https://phpstan.github.io/phpdoc-parser/2.0.x/PHPStan.PhpDocParser.Ast.Type.InvalidTypeNode.html) in place of the type.
### Removal of QuoteAwareConstExprStringNode
The class [QuoteAwareConstExprStringNode](https://phpstan.github.io/phpdoc-parser/1.23.x/PHPStan.PhpDocParser.Ast.ConstExpr.QuoteAwareConstExprStringNode.html) has been removed.
Instead, [ConstExprStringNode](https://phpstan.github.io/phpdoc-parser/2.0.x/PHPStan.PhpDocParser.Ast.ConstExpr.ConstExprStringNode.html) gained information about the kind of quotes being used.
### Removed 2nd parameter of `ConstExprParser::parse()` (`$trimStrings`)
`ConstExprStringNode::$value` now contains unescaped values without surrounding `''` or `""` quotes.
Use `ConstExprStringNode::__toString()` or [`Printer`](https://phpstan.github.io/phpdoc-parser/2.0.x/PHPStan.PhpDocParser.Printer.Printer.html) to get the escaped value along with surrounding quotes.
### Text between tags always belongs to description
Multi-line descriptions between tags were previously represented as separate [PhpDocTextNode](https://phpstan.github.io/phpdoc-parser/2.0.x/PHPStan.PhpDocParser.Ast.PhpDoc.PhpDocTextNode.html):
```php
/**
* @param Foo $foo 1st multi world description
* some text in the middle
* @param Bar $bar 2nd multi world description
*/
```
The line with `some text in the middle` in phpdoc-parser 2.0 is now part of the description of the first `@param` tag.
### `ArrayShapeNode` construction changes
`ArrayShapeNode` constructor made private, added public static methods `createSealed()` and `createUnsealed()`.
### Minor BC breaks
* Constructor parameter `$isEquality` in `AssertTag*ValueNode` made required
* Constructor parameter `$templateTypes` in `MethodTagValueNode` made required
* Constructor parameter `$isReference` in `ParamTagValueNode` made required
* Constructor parameter `$isReference` in `TypelessParamTagValueNode` made required
* Constructor parameter `$templateTypes` in `CallableTypeNode` made required
* Constructor parameters `$expectedTokenValue` and `$currentTokenLine` in `ParserException` made required
* `ArrayShapeItemNode` and `ObjectShapeItemNode` are not standalone TypeNode, just Node
@@ -0,0 +1,44 @@
{
"name": "phpstan/phpdoc-parser",
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"license": "MIT",
"require": {
"php": "^7.4 || ^8.0"
},
"require-dev": {
"doctrine/annotations": "^2.0",
"nikic/php-parser": "^5.3.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^9.6",
"symfony/process": "^5.2"
},
"config": {
"platform": {
"php": "7.4.6"
},
"sort-packages": true,
"allow-plugins": {
"phpstan/extension-installer": true
}
},
"autoload": {
"psr-4": {
"PHPStan\\PhpDocParser\\": [
"src/"
]
}
},
"autoload-dev": {
"psr-4": {
"PHPStan\\PhpDocParser\\": [
"tests/PHPStan"
]
}
},
"minimum-stability": "dev",
"prefer-stable": true
}
@@ -0,0 +1,34 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast;
/**
* Inspired by https://github.com/nikic/PHP-Parser/tree/36a6dcd04e7b0285e8f0868f44bd4927802f7df1
*
* Copyright (c) 2011, Nikita Popov
* All rights reserved.
*/
abstract class AbstractNodeVisitor implements NodeVisitor // phpcs:ignore SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming.SuperfluousPrefix
{
public function beforeTraverse(array $nodes): ?array
{
return null;
}
public function enterNode(Node $node)
{
return null;
}
public function leaveNode(Node $node)
{
return null;
}
public function afterTraverse(array $nodes): ?array
{
return null;
}
}
@@ -0,0 +1,18 @@
<?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';
public const COMMENTS = 'comments';
}
@@ -0,0 +1,28 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast;
use function trim;
class Comment
{
public string $text;
public int $startLine;
public int $startIndex;
public function __construct(string $text, int $startLine = -1, int $startIndex = -1)
{
$this->text = $text;
$this->startLine = $startLine;
$this->startIndex = $startIndex;
}
public function getReformattedText(): string
{
return trim($this->text);
}
}
@@ -0,0 +1,34 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;
class ConstExprArrayItemNode implements ConstExprNode
{
use NodeAttributes;
public ?ConstExprNode $key = null;
public ConstExprNode $value;
public function __construct(?ConstExprNode $key, ConstExprNode $value)
{
$this->key = $key;
$this->value = $value;
}
public function __toString(): string
{
if ($this->key !== null) {
return sprintf('%s => %s', $this->key, $this->value);
}
return (string) $this->value;
}
}
@@ -0,0 +1,30 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function implode;
class ConstExprArrayNode implements ConstExprNode
{
use NodeAttributes;
/** @var ConstExprArrayItemNode[] */
public array $items;
/**
* @param ConstExprArrayItemNode[] $items
*/
public function __construct(array $items)
{
$this->items = $items;
}
public function __toString(): string
{
return '[' . implode(', ', $this->items) . ']';
}
}
@@ -0,0 +1,17 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ConstExprFalseNode implements ConstExprNode
{
use NodeAttributes;
public function __toString(): string
{
return 'false';
}
}
@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ConstExprFloatNode implements ConstExprNode
{
use NodeAttributes;
public string $value;
public function __construct(string $value)
{
$this->value = $value;
}
public function __toString(): string
{
return $this->value;
}
}
@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ConstExprIntegerNode implements ConstExprNode
{
use NodeAttributes;
public string $value;
public function __construct(string $value)
{
$this->value = $value;
}
public function __toString(): string
{
return $this->value;
}
}
@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\Node;
interface ConstExprNode extends Node
{
}
@@ -0,0 +1,17 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ConstExprNullNode implements ConstExprNode
{
use NodeAttributes;
public function __toString(): string
{
return 'null';
}
}
@@ -0,0 +1,80 @@
<?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 ConstExprStringNode implements ConstExprNode
{
public const SINGLE_QUOTED = 1;
public const DOUBLE_QUOTED = 2;
use NodeAttributes;
public string $value;
/** @var self::SINGLE_QUOTED|self::DOUBLE_QUOTED */
public $quoteType;
/**
* @param self::SINGLE_QUOTED|self::DOUBLE_QUOTED $quoteType
*/
public function __construct(string $value, int $quoteType)
{
$this->value = $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);
}
}
@@ -0,0 +1,17 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ConstExprTrueNode implements ConstExprNode
{
use NodeAttributes;
public function __toString(): string
{
return 'true';
}
}
@@ -0,0 +1,34 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ConstFetchNode implements ConstExprNode
{
use NodeAttributes;
/** @var string class name for class constants or empty string for non-class constants */
public string $className;
public string $name;
public function __construct(string $className, string $name)
{
$this->className = $className;
$this->name = $name;
}
public function __toString(): string
{
if ($this->className === '') {
return $this->name;
}
return "{$this->className}::{$this->name}";
}
}
@@ -0,0 +1,41 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;
use function str_replace;
use function strlen;
use function substr;
class DoctrineConstExprStringNode extends ConstExprStringNode
{
use NodeAttributes;
public string $value;
public function __construct(string $value)
{
parent::__construct($value, self::DOUBLE_QUOTED);
$this->value = $value;
}
public function __toString(): string
{
return self::escape($this->value);
}
public static function unescape(string $value): string
{
// from https://github.com/doctrine/annotations/blob/a9ec7af212302a75d1f92fa65d3abfbd16245a2a/lib/Doctrine/Common/Annotations/DocLexer.php#L103-L107
return str_replace('""', '"', substr($value, 1, strlen($value) - 2));
}
private static function escape(string $value): string
{
// from https://github.com/phpstan/phpdoc-parser/issues/205#issuecomment-1662323656
return sprintf('"%s"', str_replace('"', '""', $value));
}
}
@@ -0,0 +1,22 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast;
interface Node
{
public function __toString(): string;
/**
* @param mixed $value
*/
public function setAttribute(string $key, $value): void;
public function hasAttribute(string $key): bool;
/**
* @return mixed
*/
public function getAttribute(string $key);
}
@@ -0,0 +1,42 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast;
use function array_key_exists;
trait NodeAttributes
{
/** @var array<string, mixed> */
private array $attributes = [];
/**
* @param mixed $value
*/
public function setAttribute(string $key, $value): void
{
if ($value === null) {
unset($this->attributes[$key]);
return;
}
$this->attributes[$key] = $value;
}
public function hasAttribute(string $key): bool
{
return array_key_exists($key, $this->attributes);
}
/**
* @return mixed
*/
public function getAttribute(string $key)
{
if ($this->hasAttribute($key)) {
return $this->attributes[$key];
}
return null;
}
}
@@ -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 array $visitors = [];
/** @var bool Whether traversal should be stopped */
private bool $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)));
}
}
}
@@ -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;
}
@@ -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
{
$node = clone $originalNode;
$node->setAttribute(Attribute::ORIGINAL_NODE, $originalNode);
return $node;
}
}
@@ -0,0 +1,45 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class AssertTagMethodValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public TypeNode $type;
public string $parameter;
public string $method;
public bool $isNegated;
public bool $isEquality;
/** @var string (may be empty) */
public string $description;
public function __construct(TypeNode $type, string $parameter, string $method, bool $isNegated, string $description, bool $isEquality)
{
$this->type = $type;
$this->parameter = $parameter;
$this->method = $method;
$this->isNegated = $isNegated;
$this->isEquality = $isEquality;
$this->description = $description;
}
public function __toString(): string
{
$isNegated = $this->isNegated ? '!' : '';
$isEquality = $this->isEquality ? '=' : '';
return trim("{$isNegated}{$isEquality}{$this->type} {$this->parameter}->{$this->method}() {$this->description}");
}
}
@@ -0,0 +1,45 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class AssertTagPropertyValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public TypeNode $type;
public string $parameter;
public string $property;
public bool $isNegated;
public bool $isEquality;
/** @var string (may be empty) */
public string $description;
public function __construct(TypeNode $type, string $parameter, string $property, bool $isNegated, string $description, bool $isEquality)
{
$this->type = $type;
$this->parameter = $parameter;
$this->property = $property;
$this->isNegated = $isNegated;
$this->isEquality = $isEquality;
$this->description = $description;
}
public function __toString(): string
{
$isNegated = $this->isNegated ? '!' : '';
$isEquality = $this->isEquality ? '=' : '';
return trim("{$isNegated}{$isEquality}{$this->type} {$this->parameter}->{$this->property} {$this->description}");
}
}
@@ -0,0 +1,42 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class AssertTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public TypeNode $type;
public string $parameter;
public bool $isNegated;
public bool $isEquality;
/** @var string (may be empty) */
public string $description;
public function __construct(TypeNode $type, string $parameter, bool $isNegated, string $description, bool $isEquality)
{
$this->type = $type;
$this->parameter = $parameter;
$this->isNegated = $isNegated;
$this->isEquality = $isEquality;
$this->description = $description;
}
public function __toString(): string
{
$isNegated = $this->isNegated ? '!' : '';
$isEquality = $this->isEquality ? '=' : '';
return trim("{$isNegated}{$isEquality}{$this->type} {$this->parameter} {$this->description}");
}
}
@@ -0,0 +1,27 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function trim;
class DeprecatedTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var string (may be empty) */
public string $description;
public function __construct(string $description)
{
$this->description = $description;
}
public function __toString(): string
{
return trim($this->description);
}
}
@@ -0,0 +1,34 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function implode;
class DoctrineAnnotation implements Node
{
use NodeAttributes;
public string $name;
/** @var list<DoctrineArgument> */
public array $arguments;
/**
* @param list<DoctrineArgument> $arguments
*/
public function __construct(string $name, array $arguments)
{
$this->name = $name;
$this->arguments = $arguments;
}
public function __toString(): string
{
$arguments = implode(', ', $this->arguments);
return $this->name . '(' . $arguments . ')';
}
}
@@ -0,0 +1,42 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
/**
* @phpstan-type ValueType = DoctrineAnnotation|IdentifierTypeNode|DoctrineArray|ConstExprNode
*/
class DoctrineArgument implements Node
{
use NodeAttributes;
public ?IdentifierTypeNode $key = null;
/** @var ValueType */
public $value;
/**
* @param ValueType $value
*/
public function __construct(?IdentifierTypeNode $key, $value)
{
$this->key = $key;
$this->value = $value;
}
public function __toString(): string
{
if ($this->key === null) {
return (string) $this->value;
}
return $this->key . '=' . $this->value;
}
}
@@ -0,0 +1,32 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function implode;
class DoctrineArray implements Node
{
use NodeAttributes;
/** @var list<DoctrineArrayItem> */
public array $items;
/**
* @param list<DoctrineArrayItem> $items
*/
public function __construct(array $items)
{
$this->items = $items;
}
public function __toString(): string
{
$items = implode(', ', $this->items);
return '{' . $items . '}';
}
}
@@ -0,0 +1,47 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
/**
* @phpstan-import-type ValueType from DoctrineArgument
* @phpstan-type KeyType = ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|ConstFetchNode|null
*/
class DoctrineArrayItem implements Node
{
use NodeAttributes;
/** @var KeyType */
public $key;
/** @var ValueType */
public $value;
/**
* @param KeyType $key
* @param ValueType $value
*/
public function __construct($key, $value)
{
$this->key = $key;
$this->value = $value;
}
public function __toString(): string
{
if ($this->key === null) {
return (string) $this->value;
}
return $this->key . '=' . $this->value;
}
}
@@ -0,0 +1,35 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use function trim;
class DoctrineTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public DoctrineAnnotation $annotation;
/** @var string (may be empty) */
public string $description;
public function __construct(
DoctrineAnnotation $annotation,
string $description
)
{
$this->annotation = $annotation;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->annotation} {$this->description}");
}
}
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use function trim;
class ExtendsTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public GenericTypeNode $type;
/** @var string (may be empty) */
public string $description;
public function __construct(GenericTypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}
@@ -0,0 +1,26 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class GenericTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var string (may be empty) */
public string $value;
public function __construct(string $value)
{
$this->value = $value;
}
public function __toString(): string
{
return $this->value;
}
}
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use function trim;
class ImplementsTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public GenericTypeNode $type;
/** @var string (may be empty) */
public string $description;
public function __construct(GenericTypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}
@@ -0,0 +1,53 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Parser\ParserException;
use function sprintf;
use function trigger_error;
use const E_USER_WARNING;
/**
* @property ParserException $exception
*/
class InvalidTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var string (may be empty) */
public string $value;
/** @var mixed[] */
private array $exceptionArgs;
public function __construct(string $value, ParserException $exception)
{
$this->value = $value;
$this->exceptionArgs = [
$exception->getCurrentTokenValue(),
$exception->getCurrentTokenType(),
$exception->getCurrentOffset(),
$exception->getExpectedTokenType(),
$exception->getExpectedTokenValue(),
$exception->getCurrentTokenLine(),
];
}
public function __get(string $name): ?ParserException
{
if ($name !== 'exception') {
trigger_error(sprintf('Undefined property: %s::$%s', self::class, $name), E_USER_WARNING);
return null;
}
return new ParserException(...$this->exceptionArgs);
}
public function __toString(): string
{
return $this->value;
}
}
@@ -0,0 +1,55 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function count;
use function implode;
class MethodTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public bool $isStatic;
public ?TypeNode $returnType = null;
public string $methodName;
/** @var TemplateTagValueNode[] */
public array $templateTypes;
/** @var MethodTagValueParameterNode[] */
public array $parameters;
/** @var string (may be empty) */
public string $description;
/**
* @param MethodTagValueParameterNode[] $parameters
* @param TemplateTagValueNode[] $templateTypes
*/
public function __construct(bool $isStatic, ?TypeNode $returnType, string $methodName, array $parameters, string $description, array $templateTypes)
{
$this->isStatic = $isStatic;
$this->returnType = $returnType;
$this->methodName = $methodName;
$this->parameters = $parameters;
$this->description = $description;
$this->templateTypes = $templateTypes;
}
public function __toString(): string
{
$static = $this->isStatic ? 'static ' : '';
$returnType = $this->returnType !== null ? "{$this->returnType} " : '';
$parameters = implode(', ', $this->parameters);
$description = $this->description !== '' ? " {$this->description}" : '';
$templateTypes = count($this->templateTypes) > 0 ? '<' . implode(', ', $this->templateTypes) . '>' : '';
return "{$static}{$returnType}{$this->methodName}{$templateTypes}({$parameters}){$description}";
}
}
@@ -0,0 +1,44 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
class MethodTagValueParameterNode implements Node
{
use NodeAttributes;
public ?TypeNode $type = null;
public bool $isReference;
public bool $isVariadic;
public string $parameterName;
public ?ConstExprNode $defaultValue = null;
public function __construct(?TypeNode $type, bool $isReference, bool $isVariadic, string $parameterName, ?ConstExprNode $defaultValue)
{
$this->type = $type;
$this->isReference = $isReference;
$this->isVariadic = $isVariadic;
$this->parameterName = $parameterName;
$this->defaultValue = $defaultValue;
}
public function __toString(): string
{
$type = $this->type !== null ? "{$this->type} " : '';
$isReference = $this->isReference ? '&' : '';
$isVariadic = $this->isVariadic ? '...' : '';
$default = $this->defaultValue !== null ? " = {$this->defaultValue}" : '';
return "{$type}{$isReference}{$isVariadic}{$this->parameterName}{$default}";
}
}
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class MixinTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public TypeNode $type;
/** @var string (may be empty) */
public string $description;
public function __construct(TypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class ParamClosureThisTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public TypeNode $type;
public string $parameterName;
/** @var string (may be empty) */
public string $description;
public function __construct(TypeNode $type, string $parameterName, string $description)
{
$this->type = $type;
$this->parameterName = $parameterName;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->parameterName} {$this->description}");
}
}
@@ -0,0 +1,29 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function trim;
class ParamImmediatelyInvokedCallableTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public string $parameterName;
/** @var string (may be empty) */
public string $description;
public function __construct(string $parameterName, string $description)
{
$this->parameterName = $parameterName;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->parameterName} {$this->description}");
}
}
@@ -0,0 +1,29 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function trim;
class ParamLaterInvokedCallableTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public string $parameterName;
/** @var string (may be empty) */
public string $description;
public function __construct(string $parameterName, string $description)
{
$this->parameterName = $parameterName;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->parameterName} {$this->description}");
}
}
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class ParamOutTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public TypeNode $type;
public string $parameterName;
/** @var string (may be empty) */
public string $description;
public function __construct(TypeNode $type, string $parameterName, string $description)
{
$this->type = $type;
$this->parameterName = $parameterName;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->parameterName} {$this->description}");
}
}
@@ -0,0 +1,42 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class ParamTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public TypeNode $type;
public bool $isReference;
public bool $isVariadic;
public string $parameterName;
/** @var string (may be empty) */
public string $description;
public function __construct(TypeNode $type, bool $isVariadic, string $parameterName, string $description, bool $isReference)
{
$this->type = $type;
$this->isReference = $isReference;
$this->isVariadic = $isVariadic;
$this->parameterName = $parameterName;
$this->description = $description;
}
public function __toString(): string
{
$reference = $this->isReference ? '&' : '';
$variadic = $this->isVariadic ? '...' : '';
return trim("{$this->type} {$reference}{$variadic}{$this->parameterName} {$this->description}");
}
}
@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\Node;
interface PhpDocChildNode extends Node
{
}
@@ -0,0 +1,401 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function array_column;
use function array_filter;
use function array_map;
use function implode;
class PhpDocNode implements Node
{
use NodeAttributes;
/** @var PhpDocChildNode[] */
public array $children;
/**
* @param PhpDocChildNode[] $children
*/
public function __construct(array $children)
{
$this->children = $children;
}
/**
* @return PhpDocTagNode[]
*/
public function getTags(): array
{
return array_filter($this->children, static fn (PhpDocChildNode $child): bool => $child instanceof PhpDocTagNode);
}
/**
* @return PhpDocTagNode[]
*/
public function getTagsByName(string $tagName): array
{
return array_filter($this->getTags(), static fn (PhpDocTagNode $tag): bool => $tag->name === $tagName);
}
/**
* @return VarTagValueNode[]
*/
public function getVarTagValues(string $tagName = '@var'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof VarTagValueNode,
);
}
/**
* @return ParamTagValueNode[]
*/
public function getParamTagValues(string $tagName = '@param'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof ParamTagValueNode,
);
}
/**
* @return TypelessParamTagValueNode[]
*/
public function getTypelessParamTagValues(string $tagName = '@param'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof TypelessParamTagValueNode,
);
}
/**
* @return ParamImmediatelyInvokedCallableTagValueNode[]
*/
public function getParamImmediatelyInvokedCallableTagValues(string $tagName = '@param-immediately-invoked-callable'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof ParamImmediatelyInvokedCallableTagValueNode,
);
}
/**
* @return ParamLaterInvokedCallableTagValueNode[]
*/
public function getParamLaterInvokedCallableTagValues(string $tagName = '@param-later-invoked-callable'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof ParamLaterInvokedCallableTagValueNode,
);
}
/**
* @return ParamClosureThisTagValueNode[]
*/
public function getParamClosureThisTagValues(string $tagName = '@param-closure-this'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof ParamClosureThisTagValueNode,
);
}
/**
* @return PureUnlessCallableIsImpureTagValueNode[]
*/
public function getPureUnlessCallableIsImpureTagValues(string $tagName = '@pure-unless-callable-is-impure'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof PureUnlessCallableIsImpureTagValueNode,
);
}
/**
* @return TemplateTagValueNode[]
*/
public function getTemplateTagValues(string $tagName = '@template'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof TemplateTagValueNode,
);
}
/**
* @return ExtendsTagValueNode[]
*/
public function getExtendsTagValues(string $tagName = '@extends'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof ExtendsTagValueNode,
);
}
/**
* @return ImplementsTagValueNode[]
*/
public function getImplementsTagValues(string $tagName = '@implements'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof ImplementsTagValueNode,
);
}
/**
* @return UsesTagValueNode[]
*/
public function getUsesTagValues(string $tagName = '@use'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof UsesTagValueNode,
);
}
/**
* @return ReturnTagValueNode[]
*/
public function getReturnTagValues(string $tagName = '@return'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof ReturnTagValueNode,
);
}
/**
* @return ThrowsTagValueNode[]
*/
public function getThrowsTagValues(string $tagName = '@throws'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof ThrowsTagValueNode,
);
}
/**
* @return MixinTagValueNode[]
*/
public function getMixinTagValues(string $tagName = '@mixin'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof MixinTagValueNode,
);
}
/**
* @return RequireExtendsTagValueNode[]
*/
public function getRequireExtendsTagValues(string $tagName = '@phpstan-require-extends'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof RequireExtendsTagValueNode,
);
}
/**
* @return RequireImplementsTagValueNode[]
*/
public function getRequireImplementsTagValues(string $tagName = '@phpstan-require-implements'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof RequireImplementsTagValueNode,
);
}
/**
* @return SealedTagValueNode[]
*/
public function getSealedTagValues(string $tagName = '@phpstan-sealed'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof SealedTagValueNode,
);
}
/**
* @return DeprecatedTagValueNode[]
*/
public function getDeprecatedTagValues(): array
{
return array_filter(
array_column($this->getTagsByName('@deprecated'), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof DeprecatedTagValueNode,
);
}
/**
* @return PropertyTagValueNode[]
*/
public function getPropertyTagValues(string $tagName = '@property'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof PropertyTagValueNode,
);
}
/**
* @return PropertyTagValueNode[]
*/
public function getPropertyReadTagValues(string $tagName = '@property-read'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof PropertyTagValueNode,
);
}
/**
* @return PropertyTagValueNode[]
*/
public function getPropertyWriteTagValues(string $tagName = '@property-write'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof PropertyTagValueNode,
);
}
/**
* @return MethodTagValueNode[]
*/
public function getMethodTagValues(string $tagName = '@method'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof MethodTagValueNode,
);
}
/**
* @return TypeAliasTagValueNode[]
*/
public function getTypeAliasTagValues(string $tagName = '@phpstan-type'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof TypeAliasTagValueNode,
);
}
/**
* @return TypeAliasImportTagValueNode[]
*/
public function getTypeAliasImportTagValues(string $tagName = '@phpstan-import-type'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof TypeAliasImportTagValueNode,
);
}
/**
* @return AssertTagValueNode[]
*/
public function getAssertTagValues(string $tagName = '@phpstan-assert'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof AssertTagValueNode,
);
}
/**
* @return AssertTagPropertyValueNode[]
*/
public function getAssertPropertyTagValues(string $tagName = '@phpstan-assert'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof AssertTagPropertyValueNode,
);
}
/**
* @return AssertTagMethodValueNode[]
*/
public function getAssertMethodTagValues(string $tagName = '@phpstan-assert'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof AssertTagMethodValueNode,
);
}
/**
* @return SelfOutTagValueNode[]
*/
public function getSelfOutTypeTagValues(string $tagName = '@phpstan-this-out'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof SelfOutTagValueNode,
);
}
/**
* @return ParamOutTagValueNode[]
*/
public function getParamOutTypeTagValues(string $tagName = '@param-out'): array
{
return array_filter(
array_column($this->getTagsByName($tagName), 'value'),
static fn (PhpDocTagValueNode $value): bool => $value instanceof ParamOutTagValueNode,
);
}
public function __toString(): string
{
$children = array_map(
static function (PhpDocChildNode $child): string {
$s = (string) $child;
return $s === '' ? '' : ' ' . $s;
},
$this->children,
);
return "/**\n *" . implode("\n *", $children) . "\n */";
}
}
@@ -0,0 +1,34 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineTagValueNode;
use function trim;
class PhpDocTagNode implements PhpDocChildNode
{
use NodeAttributes;
public string $name;
public PhpDocTagValueNode $value;
public function __construct(string $name, PhpDocTagValueNode $value)
{
$this->name = $name;
$this->value = $value;
}
public function __toString(): string
{
if ($this->value instanceof DoctrineTagValueNode) {
return (string) $this->value;
}
return trim("{$this->name} {$this->value}");
}
}
@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\Node;
interface PhpDocTagValueNode extends Node
{
}
@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class PhpDocTextNode implements PhpDocChildNode
{
use NodeAttributes;
public string $text;
public function __construct(string $text)
{
$this->text = $text;
}
public function __toString(): string
{
return $this->text;
}
}
@@ -0,0 +1,34 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class PropertyTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public TypeNode $type;
public string $propertyName;
/** @var string (may be empty) */
public string $description;
public function __construct(TypeNode $type, string $propertyName, string $description)
{
$this->type = $type;
$this->propertyName = $propertyName;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->propertyName} {$this->description}");
}
}
@@ -0,0 +1,29 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function trim;
class PureUnlessCallableIsImpureTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public string $parameterName;
/** @var string (may be empty) */
public string $description;
public function __construct(string $parameterName, string $description)
{
$this->parameterName = $parameterName;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->parameterName} {$this->description}");
}
}
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class RequireExtendsTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public TypeNode $type;
/** @var string (may be empty) */
public string $description;
public function __construct(TypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class RequireImplementsTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public TypeNode $type;
/** @var string (may be empty) */
public string $description;
public function __construct(TypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class ReturnTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public TypeNode $type;
/** @var string (may be empty) */
public string $description;
public function __construct(TypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class SealedTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public TypeNode $type;
/** @var string (may be empty) */
public string $description;
public function __construct(TypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class SelfOutTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public TypeNode $type;
/** @var string (may be empty) */
public string $description;
public function __construct(TypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim($this->type . ' ' . $this->description);
}
}
@@ -0,0 +1,47 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class TemplateTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
/** @var non-empty-string */
public string $name;
public ?TypeNode $bound;
public ?TypeNode $default;
public ?TypeNode $lowerBound;
/** @var string (may be empty) */
public string $description;
/**
* @param non-empty-string $name
*/
public function __construct(string $name, ?TypeNode $bound, string $description, ?TypeNode $default = null, ?TypeNode $lowerBound = null)
{
$this->name = $name;
$this->bound = $bound;
$this->lowerBound = $lowerBound;
$this->default = $default;
$this->description = $description;
}
public function __toString(): string
{
$upperBound = $this->bound !== null ? " of {$this->bound}" : '';
$lowerBound = $this->lowerBound !== null ? " super {$this->lowerBound}" : '';
$default = $this->default !== null ? " = {$this->default}" : '';
return trim("{$this->name}{$upperBound}{$lowerBound}{$default} {$this->description}");
}
}
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class ThrowsTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public TypeNode $type;
/** @var string (may be empty) */
public string $description;
public function __construct(TypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}
@@ -0,0 +1,35 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use function trim;
class TypeAliasImportTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public string $importedAlias;
public IdentifierTypeNode $importedFrom;
public ?string $importedAs = null;
public function __construct(string $importedAlias, IdentifierTypeNode $importedFrom, ?string $importedAs)
{
$this->importedAlias = $importedAlias;
$this->importedFrom = $importedFrom;
$this->importedAs = $importedAs;
}
public function __toString(): string
{
return trim(
"{$this->importedAlias} from {$this->importedFrom}"
. ($this->importedAs !== null ? " as {$this->importedAs}" : ''),
);
}
}
@@ -0,0 +1,30 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class TypeAliasTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public string $alias;
public TypeNode $type;
public function __construct(string $alias, TypeNode $type)
{
$this->alias = $alias;
$this->type = $type;
}
public function __toString(): string
{
return trim("{$this->alias} {$this->type}");
}
}
@@ -0,0 +1,38 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function trim;
class TypelessParamTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public bool $isReference;
public bool $isVariadic;
public string $parameterName;
/** @var string (may be empty) */
public string $description;
public function __construct(bool $isVariadic, string $parameterName, string $description, bool $isReference)
{
$this->isReference = $isReference;
$this->isVariadic = $isVariadic;
$this->parameterName = $parameterName;
$this->description = $description;
}
public function __toString(): string
{
$reference = $this->isReference ? '&' : '';
$variadic = $this->isVariadic ? '...' : '';
return trim("{$reference}{$variadic}{$this->parameterName} {$this->description}");
}
}
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use function trim;
class UsesTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public GenericTypeNode $type;
/** @var string (may be empty) */
public string $description;
public function __construct(GenericTypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}
@@ -0,0 +1,35 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function trim;
class VarTagValueNode implements PhpDocTagValueNode
{
use NodeAttributes;
public TypeNode $type;
/** @var string (may be empty) */
public string $variableName;
/** @var string (may be empty) */
public string $description;
public function __construct(TypeNode $type, string $variableName, string $description)
{
$this->type = $type;
$this->variableName = $variableName;
$this->description = $description;
}
public function __toString(): string
{
return trim("$this->type " . trim("{$this->variableName} {$this->description}"));
}
}
@@ -0,0 +1,49 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;
class ArrayShapeItemNode implements Node
{
use NodeAttributes;
/** @var ConstExprIntegerNode|ConstExprStringNode|ConstFetchNode|IdentifierTypeNode|null */
public $keyName;
public bool $optional;
public TypeNode $valueType;
/**
* @param ConstExprIntegerNode|ConstExprStringNode|ConstFetchNode|IdentifierTypeNode|null $keyName
*/
public function __construct($keyName, bool $optional, TypeNode $valueType)
{
$this->keyName = $keyName;
$this->optional = $optional;
$this->valueType = $valueType;
}
public function __toString(): string
{
if ($this->keyName !== null) {
return sprintf(
'%s%s: %s',
(string) $this->keyName,
$this->optional ? '?' : '',
(string) $this->valueType,
);
}
return (string) $this->valueType;
}
}
@@ -0,0 +1,76 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function implode;
class ArrayShapeNode implements TypeNode
{
public const KIND_ARRAY = 'array';
public const KIND_LIST = 'list';
public const KIND_NON_EMPTY_ARRAY = 'non-empty-array';
public const KIND_NON_EMPTY_LIST = 'non-empty-list';
use NodeAttributes;
/** @var ArrayShapeItemNode[] */
public array $items;
public bool $sealed;
/** @var self::KIND_* */
public $kind;
public ?ArrayShapeUnsealedTypeNode $unsealedType = null;
/**
* @param ArrayShapeItemNode[] $items
* @param self::KIND_* $kind
*/
private function __construct(
array $items,
bool $sealed = true,
?ArrayShapeUnsealedTypeNode $unsealedType = null,
string $kind = self::KIND_ARRAY
)
{
$this->items = $items;
$this->sealed = $sealed;
$this->unsealedType = $unsealedType;
$this->kind = $kind;
}
/**
* @param ArrayShapeItemNode[] $items
* @param self::KIND_* $kind
*/
public static function createSealed(array $items, string $kind = self::KIND_ARRAY): self
{
return new self($items, true, null, $kind);
}
/**
* @param ArrayShapeItemNode[] $items
* @param self::KIND_* $kind
*/
public static function createUnsealed(array $items, ?ArrayShapeUnsealedTypeNode $unsealedType, string $kind = self::KIND_ARRAY): self
{
return new self($items, false, $unsealedType, $kind);
}
public function __toString(): string
{
$items = $this->items;
if (! $this->sealed) {
$items[] = '...' . $this->unsealedType;
}
return $this->kind . '{' . implode(', ', $items) . '}';
}
}
@@ -0,0 +1,32 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;
class ArrayShapeUnsealedTypeNode implements Node
{
use NodeAttributes;
public TypeNode $valueType;
public ?TypeNode $keyType = null;
public function __construct(TypeNode $valueType, ?TypeNode $keyType)
{
$this->valueType = $valueType;
$this->keyType = $keyType;
}
public function __toString(): string
{
if ($this->keyType !== null) {
return sprintf('<%s, %s>', $this->keyType, $this->valueType);
}
return sprintf('<%s>', $this->valueType);
}
}
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ArrayTypeNode implements TypeNode
{
use NodeAttributes;
public TypeNode $type;
public function __construct(TypeNode $type)
{
$this->type = $type;
}
public function __toString(): string
{
if (
$this->type instanceof CallableTypeNode
|| $this->type instanceof ConstTypeNode
|| $this->type instanceof NullableTypeNode
) {
return '(' . $this->type . ')[]';
}
return $this->type . '[]';
}
}
@@ -0,0 +1,50 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use function implode;
class CallableTypeNode implements TypeNode
{
use NodeAttributes;
public IdentifierTypeNode $identifier;
/** @var TemplateTagValueNode[] */
public array $templateTypes;
/** @var CallableTypeParameterNode[] */
public array $parameters;
public TypeNode $returnType;
/**
* @param CallableTypeParameterNode[] $parameters
* @param TemplateTagValueNode[] $templateTypes
*/
public function __construct(IdentifierTypeNode $identifier, array $parameters, TypeNode $returnType, array $templateTypes)
{
$this->identifier = $identifier;
$this->parameters = $parameters;
$this->returnType = $returnType;
$this->templateTypes = $templateTypes;
}
public function __toString(): string
{
$returnType = $this->returnType;
if ($returnType instanceof self) {
$returnType = "({$returnType})";
}
$template = $this->templateTypes !== []
? '<' . implode(', ', $this->templateTypes) . '>'
: '';
$parameters = implode(', ', $this->parameters);
return "{$this->identifier}{$template}({$parameters}): {$returnType}";
}
}
@@ -0,0 +1,44 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function trim;
class CallableTypeParameterNode implements Node
{
use NodeAttributes;
public TypeNode $type;
public bool $isReference;
public bool $isVariadic;
/** @var string (may be empty) */
public string $parameterName;
public bool $isOptional;
public function __construct(TypeNode $type, bool $isReference, bool $isVariadic, string $parameterName, bool $isOptional)
{
$this->type = $type;
$this->isReference = $isReference;
$this->isVariadic = $isVariadic;
$this->parameterName = $parameterName;
$this->isOptional = $isOptional;
}
public function __toString(): string
{
$type = "{$this->type} ";
$isReference = $this->isReference ? '&' : '';
$isVariadic = $this->isVariadic ? '...' : '';
$isOptional = $this->isOptional ? '=' : '';
return trim("{$type}{$isReference}{$isVariadic}{$this->parameterName}") . $isOptional;
}
}
@@ -0,0 +1,44 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;
class ConditionalTypeForParameterNode implements TypeNode
{
use NodeAttributes;
public string $parameterName;
public TypeNode $targetType;
public TypeNode $if;
public TypeNode $else;
public bool $negated;
public function __construct(string $parameterName, TypeNode $targetType, TypeNode $if, TypeNode $else, bool $negated)
{
$this->parameterName = $parameterName;
$this->targetType = $targetType;
$this->if = $if;
$this->else = $else;
$this->negated = $negated;
}
public function __toString(): string
{
return sprintf(
'(%s %s %s ? %s : %s)',
$this->parameterName,
$this->negated ? 'is not' : 'is',
$this->targetType,
$this->if,
$this->else,
);
}
}
@@ -0,0 +1,44 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;
class ConditionalTypeNode implements TypeNode
{
use NodeAttributes;
public TypeNode $subjectType;
public TypeNode $targetType;
public TypeNode $if;
public TypeNode $else;
public bool $negated;
public function __construct(TypeNode $subjectType, TypeNode $targetType, TypeNode $if, TypeNode $else, bool $negated)
{
$this->subjectType = $subjectType;
$this->targetType = $targetType;
$this->if = $if;
$this->else = $else;
$this->negated = $negated;
}
public function __toString(): string
{
return sprintf(
'(%s %s %s ? %s : %s)',
$this->subjectType,
$this->negated ? 'is not' : 'is',
$this->targetType,
$this->if,
$this->else,
);
}
}
@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ConstTypeNode implements TypeNode
{
use NodeAttributes;
public ConstExprNode $constExpr;
public function __construct(ConstExprNode $constExpr)
{
$this->constExpr = $constExpr;
}
public function __toString(): string
{
return $this->constExpr->__toString();
}
}
@@ -0,0 +1,57 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function implode;
use function sprintf;
class GenericTypeNode implements TypeNode
{
public const VARIANCE_INVARIANT = 'invariant';
public const VARIANCE_COVARIANT = 'covariant';
public const VARIANCE_CONTRAVARIANT = 'contravariant';
public const VARIANCE_BIVARIANT = 'bivariant';
use NodeAttributes;
public IdentifierTypeNode $type;
/** @var TypeNode[] */
public array $genericTypes;
/** @var (self::VARIANCE_*)[] */
public array $variances;
/**
* @param TypeNode[] $genericTypes
* @param (self::VARIANCE_*)[] $variances
*/
public function __construct(IdentifierTypeNode $type, array $genericTypes, array $variances = [])
{
$this->type = $type;
$this->genericTypes = $genericTypes;
$this->variances = $variances;
}
public function __toString(): string
{
$genericTypes = [];
foreach ($this->genericTypes as $index => $type) {
$variance = $this->variances[$index] ?? self::VARIANCE_INVARIANT;
if ($variance === self::VARIANCE_INVARIANT) {
$genericTypes[] = (string) $type;
} elseif ($variance === self::VARIANCE_BIVARIANT) {
$genericTypes[] = '*';
} else {
$genericTypes[] = sprintf('%s %s', $variance, $type);
}
}
return $this->type . '<' . implode(', ', $genericTypes) . '>';
}
}
@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class IdentifierTypeNode implements TypeNode
{
use NodeAttributes;
public string $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function __toString(): string
{
return $this->name;
}
}
@@ -0,0 +1,37 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function array_map;
use function implode;
class IntersectionTypeNode implements TypeNode
{
use NodeAttributes;
/** @var TypeNode[] */
public array $types;
/**
* @param TypeNode[] $types
*/
public function __construct(array $types)
{
$this->types = $types;
}
public function __toString(): string
{
return '(' . implode(' & ', array_map(static function (TypeNode $type): string {
if ($type instanceof NullableTypeNode) {
return '(' . $type . ')';
}
return (string) $type;
}, $this->types)) . ')';
}
}
@@ -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 array $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*';
}
}
@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class NullableTypeNode implements TypeNode
{
use NodeAttributes;
public TypeNode $type;
public function __construct(TypeNode $type)
{
$this->type = $type;
}
public function __toString(): string
{
return '?' . $this->type;
}
}
@@ -0,0 +1,47 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;
class ObjectShapeItemNode implements Node
{
use NodeAttributes;
/** @var ConstExprStringNode|IdentifierTypeNode */
public $keyName;
public bool $optional;
public TypeNode $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;
}
}
@@ -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 array $items;
/**
* @param ObjectShapeItemNode[] $items
*/
public function __construct(array $items)
{
$this->items = $items;
}
public function __toString(): string
{
$items = $this->items;
return 'object{' . implode(', ', $items) . '}';
}
}
@@ -0,0 +1,34 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class OffsetAccessTypeNode implements TypeNode
{
use NodeAttributes;
public TypeNode $type;
public TypeNode $offset;
public function __construct(TypeNode $type, TypeNode $offset)
{
$this->type = $type;
$this->offset = $offset;
}
public function __toString(): string
{
if (
$this->type instanceof CallableTypeNode
|| $this->type instanceof NullableTypeNode
) {
return '(' . $this->type . ')[' . $this->offset . ']';
}
return $this->type . '[' . $this->offset . ']';
}
}
@@ -0,0 +1,17 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
class ThisTypeNode implements TypeNode
{
use NodeAttributes;
public function __toString(): string
{
return '$this';
}
}
@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\Node;
interface TypeNode extends Node
{
}
@@ -0,0 +1,37 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function array_map;
use function implode;
class UnionTypeNode implements TypeNode
{
use NodeAttributes;
/** @var TypeNode[] */
public array $types;
/**
* @param TypeNode[] $types
*/
public function __construct(array $types)
{
$this->types = $types;
}
public function __toString(): string
{
return '(' . implode(' | ', array_map(static function (TypeNode $type): string {
if ($type instanceof NullableTypeNode) {
return '(' . $type . ')';
}
return (string) $type;
}, $this->types)) . ')';
}
}
@@ -0,0 +1,199 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Lexer;
use PHPStan\PhpDocParser\ParserConfig;
use function implode;
use function preg_match_all;
use const PREG_SET_ORDER;
/**
* Implementation based on Nette Tokenizer (New BSD License; https://github.com/nette/tokenizer)
*/
class Lexer
{
public const TOKEN_REFERENCE = 0;
public const TOKEN_UNION = 1;
public const TOKEN_INTERSECTION = 2;
public const TOKEN_NULLABLE = 3;
public const TOKEN_OPEN_PARENTHESES = 4;
public const TOKEN_CLOSE_PARENTHESES = 5;
public const TOKEN_OPEN_ANGLE_BRACKET = 6;
public const TOKEN_CLOSE_ANGLE_BRACKET = 7;
public const TOKEN_OPEN_SQUARE_BRACKET = 8;
public const TOKEN_CLOSE_SQUARE_BRACKET = 9;
public const TOKEN_COMMA = 10;
public const TOKEN_VARIADIC = 11;
public const TOKEN_DOUBLE_COLON = 12;
public const TOKEN_DOUBLE_ARROW = 13;
public const TOKEN_EQUAL = 14;
public const TOKEN_OPEN_PHPDOC = 15;
public const TOKEN_CLOSE_PHPDOC = 16;
public const TOKEN_PHPDOC_TAG = 17;
public const TOKEN_DOCTRINE_TAG = 18;
public const TOKEN_FLOAT = 19;
public const TOKEN_INTEGER = 20;
public const TOKEN_SINGLE_QUOTED_STRING = 21;
public const TOKEN_DOUBLE_QUOTED_STRING = 22;
public const TOKEN_DOCTRINE_ANNOTATION_STRING = 23;
public const TOKEN_IDENTIFIER = 24;
public const TOKEN_THIS_VARIABLE = 25;
public const TOKEN_VARIABLE = 26;
public const TOKEN_HORIZONTAL_WS = 27;
public const TOKEN_PHPDOC_EOL = 28;
public const TOKEN_OTHER = 29;
public const TOKEN_END = 30;
public const TOKEN_COLON = 31;
public const TOKEN_WILDCARD = 32;
public const TOKEN_OPEN_CURLY_BRACKET = 33;
public const TOKEN_CLOSE_CURLY_BRACKET = 34;
public const TOKEN_NEGATED = 35;
public const TOKEN_ARROW = 36;
public const TOKEN_COMMENT = 37;
public const TOKEN_LABELS = [
self::TOKEN_REFERENCE => '\'&\'',
self::TOKEN_UNION => '\'|\'',
self::TOKEN_INTERSECTION => '\'&\'',
self::TOKEN_NULLABLE => '\'?\'',
self::TOKEN_NEGATED => '\'!\'',
self::TOKEN_OPEN_PARENTHESES => '\'(\'',
self::TOKEN_CLOSE_PARENTHESES => '\')\'',
self::TOKEN_OPEN_ANGLE_BRACKET => '\'<\'',
self::TOKEN_CLOSE_ANGLE_BRACKET => '\'>\'',
self::TOKEN_OPEN_SQUARE_BRACKET => '\'[\'',
self::TOKEN_CLOSE_SQUARE_BRACKET => '\']\'',
self::TOKEN_OPEN_CURLY_BRACKET => '\'{\'',
self::TOKEN_CLOSE_CURLY_BRACKET => '\'}\'',
self::TOKEN_COMMA => '\',\'',
self::TOKEN_COMMENT => '\'//\'',
self::TOKEN_COLON => '\':\'',
self::TOKEN_VARIADIC => '\'...\'',
self::TOKEN_DOUBLE_COLON => '\'::\'',
self::TOKEN_DOUBLE_ARROW => '\'=>\'',
self::TOKEN_ARROW => '\'->\'',
self::TOKEN_EQUAL => '\'=\'',
self::TOKEN_OPEN_PHPDOC => '\'/**\'',
self::TOKEN_CLOSE_PHPDOC => '\'*/\'',
self::TOKEN_PHPDOC_TAG => 'TOKEN_PHPDOC_TAG',
self::TOKEN_DOCTRINE_TAG => 'TOKEN_DOCTRINE_TAG',
self::TOKEN_PHPDOC_EOL => 'TOKEN_PHPDOC_EOL',
self::TOKEN_FLOAT => 'TOKEN_FLOAT',
self::TOKEN_INTEGER => 'TOKEN_INTEGER',
self::TOKEN_SINGLE_QUOTED_STRING => 'TOKEN_SINGLE_QUOTED_STRING',
self::TOKEN_DOUBLE_QUOTED_STRING => 'TOKEN_DOUBLE_QUOTED_STRING',
self::TOKEN_DOCTRINE_ANNOTATION_STRING => 'TOKEN_DOCTRINE_ANNOTATION_STRING',
self::TOKEN_IDENTIFIER => 'type',
self::TOKEN_THIS_VARIABLE => '\'$this\'',
self::TOKEN_VARIABLE => 'variable',
self::TOKEN_HORIZONTAL_WS => 'TOKEN_HORIZONTAL_WS',
self::TOKEN_OTHER => 'TOKEN_OTHER',
self::TOKEN_END => 'TOKEN_END',
self::TOKEN_WILDCARD => '*',
];
public const VALUE_OFFSET = 0;
public const TYPE_OFFSET = 1;
public const LINE_OFFSET = 2;
private ParserConfig $config; // @phpstan-ignore property.onlyWritten
private ?string $regexp = null;
public function __construct(ParserConfig $config)
{
$this->config = $config;
}
/**
* @return list<array{string, int, int}>
*/
public function tokenize(string $s): array
{
if ($this->regexp === null) {
$this->regexp = $this->generateRegexp();
}
preg_match_all($this->regexp, $s, $matches, PREG_SET_ORDER);
$tokens = [];
$line = 1;
foreach ($matches as $match) {
$type = (int) $match['MARK'];
$tokens[] = [$match[0], $type, $line];
if ($type !== self::TOKEN_PHPDOC_EOL) {
continue;
}
$line++;
}
$tokens[] = ['', self::TOKEN_END, $line];
return $tokens;
}
private function generateRegexp(): string
{
$patterns = [
self::TOKEN_HORIZONTAL_WS => '[\\x09\\x20]++',
self::TOKEN_IDENTIFIER => '(?:[\\\\]?+[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF-]*+)++',
self::TOKEN_THIS_VARIABLE => '\\$this(?![0-9a-z_\\x80-\\xFF])',
self::TOKEN_VARIABLE => '\\$[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF]*+',
// '&' followed by TOKEN_VARIADIC, TOKEN_VARIABLE, TOKEN_EQUAL, TOKEN_EQUAL or TOKEN_CLOSE_PARENTHESES
self::TOKEN_REFERENCE => '&(?=\\s*+(?:[.,=)]|(?:\\$(?!this(?![0-9a-z_\\x80-\\xFF])))))',
self::TOKEN_UNION => '\\|',
self::TOKEN_INTERSECTION => '&',
self::TOKEN_NULLABLE => '\\?',
self::TOKEN_NEGATED => '!',
self::TOKEN_OPEN_PARENTHESES => '\\(',
self::TOKEN_CLOSE_PARENTHESES => '\\)',
self::TOKEN_OPEN_ANGLE_BRACKET => '<',
self::TOKEN_CLOSE_ANGLE_BRACKET => '>',
self::TOKEN_OPEN_SQUARE_BRACKET => '\\[',
self::TOKEN_CLOSE_SQUARE_BRACKET => '\\]',
self::TOKEN_OPEN_CURLY_BRACKET => '\\{',
self::TOKEN_CLOSE_CURLY_BRACKET => '\\}',
self::TOKEN_COMMA => ',',
self::TOKEN_COMMENT => '\/\/[^\\r\\n]*(?=\n|\r|\*/)',
self::TOKEN_VARIADIC => '\\.\\.\\.',
self::TOKEN_DOUBLE_COLON => '::',
self::TOKEN_DOUBLE_ARROW => '=>',
self::TOKEN_ARROW => '->',
self::TOKEN_EQUAL => '=',
self::TOKEN_COLON => ':',
self::TOKEN_OPEN_PHPDOC => '/\\*\\*(?=\\s)\\x20?+',
self::TOKEN_CLOSE_PHPDOC => '\\*/',
self::TOKEN_PHPDOC_TAG => '@(?:[a-z][a-z0-9-\\\\]+:)?[a-z][a-z0-9-\\\\]*+',
self::TOKEN_DOCTRINE_TAG => '@[a-z_\\\\][a-z0-9_\:\\\\]*[a-z_][a-z0-9_]*',
self::TOKEN_PHPDOC_EOL => '\\r?+\\n[\\x09\\x20]*+(?:\\*(?!/)\\x20?+)?',
self::TOKEN_FLOAT => '[+\-]?(?:(?:[0-9]++(_[0-9]++)*\\.[0-9]*+(_[0-9]++)*(?:e[+\-]?[0-9]++(_[0-9]++)*)?)|(?:[0-9]*+(_[0-9]++)*\\.[0-9]++(_[0-9]++)*(?:e[+\-]?[0-9]++(_[0-9]++)*)?)|(?:[0-9]++(_[0-9]++)*e[+\-]?[0-9]++(_[0-9]++)*))',
self::TOKEN_INTEGER => '[+\-]?(?:(?:0b[0-1]++(_[0-1]++)*)|(?:0o[0-7]++(_[0-7]++)*)|(?:0x[0-9a-f]++(_[0-9a-f]++)*)|(?:[0-9]++(_[0-9]++)*))',
self::TOKEN_SINGLE_QUOTED_STRING => '\'(?:\\\\[^\\r\\n]|[^\'\\r\\n\\\\])*+\'',
self::TOKEN_DOUBLE_QUOTED_STRING => '"(?:\\\\[^\\r\\n]|[^"\\r\\n\\\\])*+"',
self::TOKEN_DOCTRINE_ANNOTATION_STRING => '"(?:""|[^"])*+"',
self::TOKEN_WILDCARD => '\\*',
// anything but TOKEN_CLOSE_PHPDOC or TOKEN_HORIZONTAL_WS or TOKEN_EOL
self::TOKEN_OTHER => '(?:(?!\\*/)[^\\s])++',
];
foreach ($patterns as $type => &$pattern) {
$pattern = '(?:' . $pattern . ')(*MARK:' . $type . ')';
}
return '~' . implode('|', $patterns) . '~Asi';
}
}
@@ -0,0 +1,292 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Parser;
use PHPStan\PhpDocParser\Ast;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\ParserConfig;
use function str_replace;
use function strtolower;
class ConstExprParser
{
private ParserConfig $config;
private bool $parseDoctrineStrings;
public function __construct(
ParserConfig $config
)
{
$this->config = $config;
$this->parseDoctrineStrings = false;
}
/**
* @internal
*/
public function toDoctrine(): self
{
$self = new self($this->config);
$self->parseDoctrineStrings = true;
return $self;
}
public function parse(TokenIterator $tokens): Ast\ConstExpr\ConstExprNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_FLOAT)) {
$value = $tokens->currentTokenValue();
$tokens->next();
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprFloatNode(str_replace('_', '', $value)),
$startLine,
$startIndex,
);
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
$value = $tokens->currentTokenValue();
$tokens->next();
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $value)),
$startLine,
$startIndex,
);
}
if ($this->parseDoctrineStrings && $tokens->isCurrentTokenType(Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) {
$value = $tokens->currentTokenValue();
$tokens->next();
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\DoctrineConstExprStringNode(Ast\ConstExpr\DoctrineConstExprStringNode::unescape($value)),
$startLine,
$startIndex,
);
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING, Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
if ($this->parseDoctrineStrings) {
if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
throw new ParserException(
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Lexer::TOKEN_DOUBLE_QUOTED_STRING,
null,
$tokens->currentTokenLine(),
);
}
$value = $tokens->currentTokenValue();
$tokens->next();
return $this->enrichWithAttributes(
$tokens,
$this->parseDoctrineString($value, $tokens),
$startLine,
$startIndex,
);
}
$value = StringUnescaper::unescapeString($tokens->currentTokenValue());
$type = $tokens->currentTokenType();
$tokens->next();
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprStringNode(
$value,
$type === Lexer::TOKEN_SINGLE_QUOTED_STRING
? Ast\ConstExpr\ConstExprStringNode::SINGLE_QUOTED
: Ast\ConstExpr\ConstExprStringNode::DOUBLE_QUOTED,
),
$startLine,
$startIndex,
);
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
$identifier = $tokens->currentTokenValue();
$tokens->next();
switch (strtolower($identifier)) {
case 'true':
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprTrueNode(),
$startLine,
$startIndex,
);
case 'false':
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprFalseNode(),
$startLine,
$startIndex,
);
case 'null':
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprNullNode(),
$startLine,
$startIndex,
);
case 'array':
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_PARENTHESES, $startIndex);
}
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
$classConstantName = '';
$lastType = null;
while (true) {
if ($lastType !== Lexer::TOKEN_IDENTIFIER && $tokens->currentTokenType() === Lexer::TOKEN_IDENTIFIER) {
$classConstantName .= $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
$lastType = Lexer::TOKEN_IDENTIFIER;
continue;
}
if ($lastType !== Lexer::TOKEN_WILDCARD && $tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) {
$classConstantName .= '*';
$lastType = Lexer::TOKEN_WILDCARD;
if ($tokens->getSkippedHorizontalWhiteSpaceIfAny() !== '') {
break;
}
continue;
}
if ($lastType === null) {
// trigger parse error if nothing valid was consumed
$tokens->consumeTokenType(Lexer::TOKEN_WILDCARD);
}
break;
}
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstFetchNode($identifier, $classConstantName),
$startLine,
$startIndex,
);
}
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstFetchNode('', $identifier),
$startLine,
$startIndex,
);
} elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_SQUARE_BRACKET, $startIndex);
}
throw new ParserException(
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Lexer::TOKEN_IDENTIFIER,
null,
$tokens->currentTokenLine(),
);
}
private function parseArray(TokenIterator $tokens, int $endToken, int $startIndex): Ast\ConstExpr\ConstExprArrayNode
{
$items = [];
$startLine = $tokens->currentTokenLine();
if (!$tokens->tryConsumeTokenType($endToken)) {
do {
$items[] = $this->parseArrayItem($tokens);
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA) && !$tokens->isCurrentTokenType($endToken));
$tokens->consumeTokenType($endToken);
}
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprArrayNode($items),
$startLine,
$startIndex,
);
}
/**
* This method is supposed to be called with TokenIterator after reading TOKEN_DOUBLE_QUOTED_STRING and shifting
* to the next token.
*/
public function parseDoctrineString(string $text, TokenIterator $tokens): Ast\ConstExpr\DoctrineConstExprStringNode
{
// Because of how Lexer works, a valid Doctrine string
// can consist of a sequence of TOKEN_DOUBLE_QUOTED_STRING and TOKEN_DOCTRINE_ANNOTATION_STRING
while ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING, Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) {
$text .= $tokens->currentTokenValue();
$tokens->next();
}
return new Ast\ConstExpr\DoctrineConstExprStringNode(Ast\ConstExpr\DoctrineConstExprStringNode::unescape($text));
}
private function parseArrayItem(TokenIterator $tokens): Ast\ConstExpr\ConstExprArrayItemNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
$expr = $this->parse($tokens);
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_ARROW)) {
$key = $expr;
$value = $this->parse($tokens);
} else {
$key = null;
$value = $expr;
}
return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\ConstExprArrayItemNode($key, $value),
$startLine,
$startIndex,
);
}
/**
* @template T of Ast\ConstExpr\ConstExprNode
* @param T $node
* @return T
*/
private function enrichWithAttributes(TokenIterator $tokens, Ast\ConstExpr\ConstExprNode $node, int $startLine, int $startIndex): Ast\ConstExpr\ConstExprNode
{
if ($this->config->useLinesAttributes) {
$node->setAttribute(Ast\Attribute::START_LINE, $startLine);
$node->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine());
}
if ($this->config->useIndexAttributes) {
$node->setAttribute(Ast\Attribute::START_INDEX, $startIndex);
$node->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken());
}
return $node;
}
}
@@ -0,0 +1,100 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Parser;
use Exception;
use PHPStan\PhpDocParser\Lexer\Lexer;
use function assert;
use function json_encode;
use function sprintf;
use const JSON_INVALID_UTF8_SUBSTITUTE;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;
class ParserException extends Exception
{
private string $currentTokenValue;
private int $currentTokenType;
private int $currentOffset;
private int $expectedTokenType;
private ?string $expectedTokenValue;
private ?int $currentTokenLine;
public function __construct(
string $currentTokenValue,
int $currentTokenType,
int $currentOffset,
int $expectedTokenType,
?string $expectedTokenValue,
?int $currentTokenLine
)
{
$this->currentTokenValue = $currentTokenValue;
$this->currentTokenType = $currentTokenType;
$this->currentOffset = $currentOffset;
$this->expectedTokenType = $expectedTokenType;
$this->expectedTokenValue = $expectedTokenValue;
$this->currentTokenLine = $currentTokenLine;
parent::__construct(sprintf(
'Unexpected token %s, expected %s%s at offset %d%s',
$this->formatValue($currentTokenValue),
Lexer::TOKEN_LABELS[$expectedTokenType],
$expectedTokenValue !== null ? sprintf(' (%s)', $this->formatValue($expectedTokenValue)) : '',
$currentOffset,
$currentTokenLine === null ? '' : sprintf(' on line %d', $currentTokenLine),
));
}
public function getCurrentTokenValue(): string
{
return $this->currentTokenValue;
}
public function getCurrentTokenType(): int
{
return $this->currentTokenType;
}
public function getCurrentOffset(): int
{
return $this->currentOffset;
}
public function getExpectedTokenType(): int
{
return $this->expectedTokenType;
}
public function getExpectedTokenValue(): ?string
{
return $this->expectedTokenValue;
}
public function getCurrentTokenLine(): ?int
{
return $this->currentTokenLine;
}
private function formatValue(string $value): string
{
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
assert($json !== false);
return $json;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,100 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Parser;
use PHPStan\ShouldNotHappenException;
use function chr;
use function hexdec;
use function octdec;
use function preg_replace_callback;
use function str_replace;
use function substr;
class StringUnescaper
{
private const REPLACEMENTS = [
'\\' => '\\',
'n' => "\n",
'r' => "\r",
't' => "\t",
'f' => "\f",
'v' => "\v",
'e' => "\x1B",
];
public static function unescapeString(string $string): string
{
$quote = $string[0];
if ($quote === '\'') {
return str_replace(
['\\\\', '\\\''],
['\\', '\''],
substr($string, 1, -1),
);
}
return self::parseEscapeSequences(substr($string, 1, -1), '"');
}
/**
* Implementation based on https://github.com/nikic/PHP-Parser/blob/b0edd4c41111042d43bb45c6c657b2e0db367d9e/lib/PhpParser/Node/Scalar/String_.php#L90-L130
*/
private static function parseEscapeSequences(string $str, string $quote): string
{
$str = str_replace('\\' . $quote, $quote, $str);
return preg_replace_callback(
'~\\\\([\\\\nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3}|u\{([0-9a-fA-F]+)\})~',
static function ($matches) {
$str = $matches[1];
if (isset(self::REPLACEMENTS[$str])) {
return self::REPLACEMENTS[$str];
}
if ($str[0] === 'x' || $str[0] === 'X') {
return chr((int) hexdec(substr($str, 1)));
}
if ($str[0] === 'u') {
if (!isset($matches[2])) {
throw new ShouldNotHappenException();
}
return self::codePointToUtf8((int) hexdec($matches[2]));
}
return chr((int) octdec($str));
},
$str,
);
}
/**
* Implementation based on https://github.com/nikic/PHP-Parser/blob/b0edd4c41111042d43bb45c6c657b2e0db367d9e/lib/PhpParser/Node/Scalar/String_.php#L132-L154
*/
private static function codePointToUtf8(int $num): string
{
if ($num <= 0x7F) {
return chr($num);
}
if ($num <= 0x7FF) {
return chr(($num >> 6) + 0xC0)
. chr(($num & 0x3F) + 0x80);
}
if ($num <= 0xFFFF) {
return chr(($num >> 12) + 0xE0)
. chr((($num >> 6) & 0x3F) + 0x80)
. chr(($num & 0x3F) + 0x80);
}
if ($num <= 0x1FFFFF) {
return chr(($num >> 18) + 0xF0)
. chr((($num >> 12) & 0x3F) + 0x80)
. chr((($num >> 6) & 0x3F) + 0x80)
. chr(($num & 0x3F) + 0x80);
}
// Invalid UTF-8 codepoint escape sequence: Codepoint too large
return "\xef\xbf\xbd";
}
}
@@ -0,0 +1,428 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Parser;
use LogicException;
use PHPStan\PhpDocParser\Ast\Comment;
use PHPStan\PhpDocParser\Lexer\Lexer;
use function array_pop;
use function assert;
use function count;
use function in_array;
use function strlen;
use function substr;
class TokenIterator
{
/** @var list<array{string, int, int}> */
private array $tokens;
private int $index;
/** @var list<Comment> */
private array $comments = [];
/** @var list<array{int, list<Comment>}> */
private array $savePoints = [];
/** @var list<int> */
private array $skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS];
private ?string $newline = null;
/**
* @param list<array{string, int, int}> $tokens
*/
public function __construct(array $tokens, int $index = 0)
{
$this->tokens = $tokens;
$this->index = $index;
$this->skipIrrelevantTokens();
}
/**
* @return list<array{string, int, int}>
*/
public function getTokens(): array
{
return $this->tokens;
}
public function getContentBetween(int $startPos, int $endPos): string
{
if ($startPos < 0 || $endPos > count($this->tokens)) {
throw new LogicException();
}
$content = '';
for ($i = $startPos; $i < $endPos; $i++) {
$content .= $this->tokens[$i][Lexer::VALUE_OFFSET];
}
return $content;
}
public function getTokenCount(): int
{
return count($this->tokens);
}
public function currentTokenValue(): string
{
return $this->tokens[$this->index][Lexer::VALUE_OFFSET];
}
public function currentTokenType(): int
{
return $this->tokens[$this->index][Lexer::TYPE_OFFSET];
}
public function currentTokenOffset(): int
{
$offset = 0;
for ($i = 0; $i < $this->index; $i++) {
$offset += strlen($this->tokens[$i][Lexer::VALUE_OFFSET]);
}
return $offset;
}
public function currentTokenLine(): int
{
return $this->tokens[$this->index][Lexer::LINE_OFFSET];
}
public function currentTokenIndex(): int
{
return $this->index;
}
public function endIndexOfLastRelevantToken(): int
{
$endIndex = $this->currentTokenIndex();
$endIndex--;
while (in_array($this->tokens[$endIndex][Lexer::TYPE_OFFSET], $this->skippedTokenTypes, true)) {
if (!isset($this->tokens[$endIndex - 1])) {
break;
}
$endIndex--;
}
return $endIndex;
}
public function isCurrentTokenValue(string $tokenValue): bool
{
return $this->tokens[$this->index][Lexer::VALUE_OFFSET] === $tokenValue;
}
public function isCurrentTokenType(int ...$tokenType): bool
{
return in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $tokenType, true);
}
public function isPrecededByHorizontalWhitespace(): bool
{
return ($this->tokens[$this->index - 1][Lexer::TYPE_OFFSET] ?? -1) === Lexer::TOKEN_HORIZONTAL_WS;
}
/**
* @throws ParserException
*/
public function consumeTokenType(int $tokenType): void
{
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType) {
$this->throwError($tokenType);
}
if ($tokenType === Lexer::TOKEN_PHPDOC_EOL) {
if ($this->newline === null) {
$this->detectNewline();
}
}
$this->next();
}
/**
* @throws ParserException
*/
public function consumeTokenValue(int $tokenType, string $tokenValue): void
{
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType || $this->tokens[$this->index][Lexer::VALUE_OFFSET] !== $tokenValue) {
$this->throwError($tokenType, $tokenValue);
}
$this->next();
}
/** @phpstan-impure */
public function tryConsumeTokenValue(string $tokenValue): bool
{
if ($this->tokens[$this->index][Lexer::VALUE_OFFSET] !== $tokenValue) {
return false;
}
$this->next();
return true;
}
/**
* @return list<Comment>
*/
public function flushComments(): array
{
$res = $this->comments;
$this->comments = [];
return $res;
}
/** @phpstan-impure */
public function tryConsumeTokenType(int $tokenType): bool
{
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType) {
return false;
}
if ($tokenType === Lexer::TOKEN_PHPDOC_EOL) {
if ($this->newline === null) {
$this->detectNewline();
}
}
$this->next();
return true;
}
/**
* @deprecated Use skipNewLineTokensAndConsumeComments instead (when parsing a type)
*/
public function skipNewLineTokens(): void
{
if (!$this->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
return;
}
do {
$foundNewLine = $this->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
} while ($foundNewLine === true);
}
public function skipNewLineTokensAndConsumeComments(): void
{
if ($this->currentTokenType() === Lexer::TOKEN_COMMENT) {
$this->comments[] = new Comment($this->currentTokenValue(), $this->currentTokenLine(), $this->currentTokenIndex());
$this->next();
}
if (!$this->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
return;
}
do {
$foundNewLine = $this->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
if ($this->currentTokenType() !== Lexer::TOKEN_COMMENT) {
continue;
}
$this->comments[] = new Comment($this->currentTokenValue(), $this->currentTokenLine(), $this->currentTokenIndex());
$this->next();
} while ($foundNewLine === true);
}
private function detectNewline(): void
{
$value = $this->currentTokenValue();
if (substr($value, 0, 2) === "\r\n") {
$this->newline = "\r\n";
} elseif (substr($value, 0, 1) === "\n") {
$this->newline = "\n";
}
}
public function getSkippedHorizontalWhiteSpaceIfAny(): string
{
if ($this->index > 0 && $this->tokens[$this->index - 1][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) {
return $this->tokens[$this->index - 1][Lexer::VALUE_OFFSET];
}
return '';
}
/** @phpstan-impure */
public function joinUntil(int ...$tokenType): string
{
$s = '';
while (!in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $tokenType, true)) {
$s .= $this->tokens[$this->index++][Lexer::VALUE_OFFSET];
}
return $s;
}
public function next(): void
{
$this->index++;
$this->skipIrrelevantTokens();
}
private function skipIrrelevantTokens(): void
{
if (!isset($this->tokens[$this->index])) {
return;
}
while (in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $this->skippedTokenTypes, true)) {
if (!isset($this->tokens[$this->index + 1])) {
break;
}
$this->index++;
}
}
public function addEndOfLineToSkippedTokens(): void
{
$this->skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS, Lexer::TOKEN_PHPDOC_EOL];
}
public function removeEndOfLineFromSkippedTokens(): void
{
$this->skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS];
}
/** @phpstan-impure */
public function forwardToTheEnd(): void
{
$lastToken = count($this->tokens) - 1;
$this->index = $lastToken;
}
public function pushSavePoint(): void
{
$this->savePoints[] = [$this->index, $this->comments];
}
public function dropSavePoint(): void
{
array_pop($this->savePoints);
}
public function rollback(): void
{
$savepoint = array_pop($this->savePoints);
assert($savepoint !== null);
[$this->index, $this->comments] = $savepoint;
}
/**
* @throws ParserException
*/
private function throwError(int $expectedTokenType, ?string $expectedTokenValue = null): void
{
throw new ParserException(
$this->currentTokenValue(),
$this->currentTokenType(),
$this->currentTokenOffset(),
$expectedTokenType,
$expectedTokenValue,
$this->currentTokenLine(),
);
}
/**
* Check whether the position is directly preceded by a certain token type.
*
* During this check TOKEN_HORIZONTAL_WS and TOKEN_PHPDOC_EOL are skipped
*/
public function hasTokenImmediatelyBefore(int $pos, int $expectedTokenType): bool
{
$tokens = $this->tokens;
$pos--;
for (; $pos >= 0; $pos--) {
$token = $tokens[$pos];
$type = $token[Lexer::TYPE_OFFSET];
if ($type === $expectedTokenType) {
return true;
}
if (!in_array($type, [
Lexer::TOKEN_HORIZONTAL_WS,
Lexer::TOKEN_PHPDOC_EOL,
], true)) {
break;
}
}
return false;
}
/**
* Check whether the position is directly followed by a certain token type.
*
* During this check TOKEN_HORIZONTAL_WS and TOKEN_PHPDOC_EOL are skipped
*/
public function hasTokenImmediatelyAfter(int $pos, int $expectedTokenType): bool
{
$tokens = $this->tokens;
$pos++;
for ($c = count($tokens); $pos < $c; $pos++) {
$token = $tokens[$pos];
$type = $token[Lexer::TYPE_OFFSET];
if ($type === $expectedTokenType) {
return true;
}
if (!in_array($type, [
Lexer::TOKEN_HORIZONTAL_WS,
Lexer::TOKEN_PHPDOC_EOL,
], true)) {
break;
}
}
return false;
}
public function getDetectedNewline(): ?string
{
return $this->newline;
}
/**
* Whether the given position is immediately surrounded by parenthesis.
*/
public function hasParentheses(int $startPos, int $endPos): bool
{
return $this->hasTokenImmediatelyBefore($startPos, Lexer::TOKEN_OPEN_PARENTHESES)
&& $this->hasTokenImmediatelyAfter($endPos, Lexer::TOKEN_CLOSE_PARENTHESES);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,24 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser;
class ParserConfig
{
public bool $useLinesAttributes;
public bool $useIndexAttributes;
public bool $useCommentsAttributes;
/**
* @param array{lines?: bool, indexes?: bool, comments?: bool} $usedAttributes
*/
public function __construct(array $usedAttributes)
{
$this->useLinesAttributes = $usedAttributes['lines'] ?? false;
$this->useIndexAttributes = $usedAttributes['indexes'] ?? false;
$this->useCommentsAttributes = $usedAttributes['comments'] ?? false;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -0,0 +1,920 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Printer;
use LogicException;
use PHPStan\PhpDocParser\Ast\Attribute;
use PHPStan\PhpDocParser\Ast\Comment;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagMethodValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagPropertyValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineAnnotation;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArgument;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArray;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArrayItem;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ImplementsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamClosureThisTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamOutTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\SealedTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasImportTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\UsesTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeUnsealedTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
use PHPStan\PhpDocParser\Ast\Type\InvalidTypeNode;
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode;
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use function array_keys;
use function array_map;
use function assert;
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 str_replace;
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 $differ;
/**
* Map From "{$class}->{$subNode}" to string that should be inserted
* between elements of this list subnode
*
* @var array<string, string>
*/
private array $listInsertionMap = [
PhpDocNode::class . '->children' => "\n * ",
UnionTypeNode::class . '->types' => '|',
IntersectionTypeNode::class . '->types' => '&',
ArrayShapeNode::class . '->items' => ', ',
ObjectShapeNode::class . '->items' => ', ',
CallableTypeNode::class . '->parameters' => ', ',
CallableTypeNode::class . '->templateTypes' => ', ',
GenericTypeNode::class . '->genericTypes' => ', ',
ConstExprArrayNode::class . '->items' => ', ',
MethodTagValueNode::class . '->parameters' => ', ',
DoctrineArray::class . '->items' => ', ',
DoctrineAnnotation::class . '->arguments' => ', ',
];
/**
* [$find, $extraLeft, $extraRight]
*
* @var array<string, array{string|null, string, string}>
*/
private array $emptyListInsertionMap = [
CallableTypeNode::class . '->parameters' => ['(', '', ''],
ArrayShapeNode::class . '->items' => ['{', '', ''],
ObjectShapeNode::class . '->items' => ['{', '', ''],
DoctrineArray::class . '->items' => ['{', '', ''],
DoctrineAnnotation::class . '->arguments' => ['(', '', ''],
];
/** @var array<string, list<class-string<TypeNode>>> */
private array $parenthesesMap = [
CallableTypeNode::class . '->returnType' => [
CallableTypeNode::class,
UnionTypeNode::class,
IntersectionTypeNode::class,
],
ArrayTypeNode::class . '->type' => [
CallableTypeNode::class,
UnionTypeNode::class,
IntersectionTypeNode::class,
ConstTypeNode::class,
NullableTypeNode::class,
],
OffsetAccessTypeNode::class . '->type' => [
CallableTypeNode::class,
UnionTypeNode::class,
IntersectionTypeNode::class,
NullableTypeNode::class,
],
];
/** @var array<string, list<class-string<TypeNode>>> */
private array $parenthesesListMap = [
IntersectionTypeNode::class . '->types' => [
IntersectionTypeNode::class,
UnionTypeNode::class,
NullableTypeNode::class,
],
UnionTypeNode::class . '->types' => [
IntersectionTypeNode::class,
UnionTypeNode::class,
NullableTypeNode::class,
],
];
public function printFormatPreserving(PhpDocNode $node, PhpDocNode $originalNode, TokenIterator $originalTokens): string
{
$this->differ = new Differ(static function ($a, $b) {
if ($a instanceof Node && $b instanceof Node) {
return $a === $b->getAttribute(Attribute::ORIGINAL_NODE);
}
return false;
});
$tokenIndex = 0;
$result = $this->printArrayFormatPreserving(
$node->children,
$originalNode->children,
$originalTokens,
$tokenIndex,
PhpDocNode::class,
'children',
);
if ($result !== null) {
return $result . $originalTokens->getContentBetween($tokenIndex, $originalTokens->getTokenCount());
}
return $this->print($node);
}
public function print(Node $node): string
{
if ($node instanceof PhpDocNode) {
return "/**\n *" . implode("\n *", array_map(
function (PhpDocChildNode $child): string {
$s = $this->print($child);
return $s === '' ? '' : ' ' . $s;
},
$node->children,
)) . "\n */";
}
if ($node instanceof PhpDocTextNode) {
return $node->text;
}
if ($node instanceof PhpDocTagNode) {
if ($node->value instanceof DoctrineTagValueNode) {
return $this->print($node->value);
}
return trim(sprintf('%s %s', $node->name, $this->print($node->value)));
}
if ($node instanceof PhpDocTagValueNode) {
return $this->printTagValue($node);
}
if ($node instanceof TypeNode) {
return $this->printType($node);
}
if ($node instanceof ConstExprNode) {
return $this->printConstExpr($node);
}
if ($node instanceof MethodTagValueParameterNode) {
$type = $node->type !== null ? $this->print($node->type) . ' ' : '';
$isReference = $node->isReference ? '&' : '';
$isVariadic = $node->isVariadic ? '...' : '';
$default = $node->defaultValue !== null ? ' = ' . $this->print($node->defaultValue) : '';
return "{$type}{$isReference}{$isVariadic}{$node->parameterName}{$default}";
}
if ($node instanceof CallableTypeParameterNode) {
$type = $this->print($node->type) . ' ';
$isReference = $node->isReference ? '&' : '';
$isVariadic = $node->isVariadic ? '...' : '';
$isOptional = $node->isOptional ? '=' : '';
return trim("{$type}{$isReference}{$isVariadic}{$node->parameterName}") . $isOptional;
}
if ($node instanceof ArrayShapeUnsealedTypeNode) {
if ($node->keyType !== null) {
return sprintf('<%s, %s>', $this->printType($node->keyType), $this->printType($node->valueType));
}
return sprintf('<%s>', $this->printType($node->valueType));
}
if ($node instanceof DoctrineAnnotation) {
return (string) $node;
}
if ($node instanceof DoctrineArgument) {
return (string) $node;
}
if ($node instanceof DoctrineArray) {
return (string) $node;
}
if ($node instanceof DoctrineArrayItem) {
return (string) $node;
}
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 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);
}
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(fn (MethodTagValueParameterNode $parameter): string => $this->print($parameter), $node->parameters));
$description = $node->description !== '' ? " {$node->description}" : '';
$templateTypes = count($node->templateTypes) > 0 ? '<' . implode(', ', array_map(fn (TemplateTagValueNode $templateTag): string => $this->print($templateTag), $node->templateTypes)) . '>' : '';
return "{$static}{$returnType}{$node->methodName}{$templateTypes}({$parameters}){$description}";
}
if ($node instanceof MixinTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->description}");
}
if ($node instanceof RequireExtendsTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->description}");
}
if ($node instanceof RequireImplementsTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->description}");
}
if ($node instanceof SealedTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->description}");
}
if ($node instanceof ParamOutTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->parameterName} {$node->description}");
}
if ($node instanceof ParamTagValueNode) {
$reference = $node->isReference ? '&' : '';
$variadic = $node->isVariadic ? '...' : '';
$type = $this->printType($node->type);
return trim("{$type} {$reference}{$variadic}{$node->parameterName} {$node->description}");
}
if ($node instanceof ParamImmediatelyInvokedCallableTagValueNode) {
return trim("{$node->parameterName} {$node->description}");
}
if ($node instanceof ParamLaterInvokedCallableTagValueNode) {
return trim("{$node->parameterName} {$node->description}");
}
if ($node instanceof ParamClosureThisTagValueNode) {
return trim("{$node->type} {$node->parameterName} {$node->description}");
}
if ($node instanceof PureUnlessCallableIsImpureTagValueNode) {
return trim("{$node->parameterName} {$node->description}");
}
if ($node instanceof PropertyTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->propertyName} {$node->description}");
}
if ($node instanceof ReturnTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->description}");
}
if ($node instanceof SelfOutTagValueNode) {
$type = $this->printType($node->type);
return trim($type . ' ' . $node->description);
}
if ($node instanceof TemplateTagValueNode) {
$upperBound = $node->bound !== null ? ' of ' . $this->printType($node->bound) : '';
$lowerBound = $node->lowerBound !== null ? ' super ' . $this->printType($node->lowerBound) : '';
$default = $node->default !== null ? ' = ' . $this->printType($node->default) : '';
return trim("{$node->name}{$upperBound}{$lowerBound}{$default} {$node->description}");
}
if ($node instanceof ThrowsTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->description}");
}
if ($node instanceof TypeAliasImportTagValueNode) {
return trim(
"{$node->importedAlias} from " . $this->printType($node->importedFrom)
. ($node->importedAs !== null ? " as {$node->importedAs}" : ''),
);
}
if ($node instanceof TypeAliasTagValueNode) {
$type = $this->printType($node->type);
return trim("{$node->alias} {$type}");
}
if ($node instanceof UsesTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} {$node->description}");
}
if ($node instanceof VarTagValueNode) {
$type = $this->printType($node->type);
return trim("{$type} " . trim("{$node->variableName} {$node->description}"));
}
return (string) $node;
}
private function printType(TypeNode $node): string
{
if ($node instanceof ArrayShapeNode) {
$items = array_map(fn (ArrayShapeItemNode $item): string => $this->print($item), $node->items);
if (! $node->sealed) {
$items[] = '...' . ($node->unsealedType === null ? '' : $this->print($node->unsealedType));
}
return $node->kind . '{' . implode(', ', $items) . '}';
}
if ($node instanceof ArrayTypeNode) {
return $this->printOffsetAccessType($node->type) . '[]';
}
if ($node instanceof CallableTypeNode) {
if ($node->returnType instanceof CallableTypeNode || $node->returnType instanceof UnionTypeNode || $node->returnType instanceof IntersectionTypeNode) {
$returnType = $this->wrapInParentheses($node->returnType);
} else {
$returnType = $this->printType($node->returnType);
}
$template = $node->templateTypes !== []
? '<' . implode(', ', array_map(fn (TemplateTagValueNode $templateNode): string => $this->print($templateNode), $node->templateTypes)) . '>'
: '';
$parameters = implode(', ', array_map(fn (CallableTypeParameterNode $parameterNode): string => $this->print($parameterNode), $node->parameters));
return "{$node->identifier}{$template}({$parameters}): {$returnType}";
}
if ($node instanceof ConditionalTypeForParameterNode) {
return sprintf(
'(%s %s %s ? %s : %s)',
$node->parameterName,
$node->negated ? 'is not' : 'is',
$this->printType($node->targetType),
$this->printType($node->if),
$this->printType($node->else),
);
}
if ($node instanceof ConditionalTypeNode) {
return sprintf(
'(%s %s %s ? %s : %s)',
$this->printType($node->subjectType),
$node->negated ? 'is not' : 'is',
$this->printType($node->targetType),
$this->printType($node->if),
$this->printType($node->else),
);
}
if ($node instanceof ConstTypeNode) {
return $this->printConstExpr($node->constExpr);
}
if ($node instanceof GenericTypeNode) {
$genericTypes = [];
foreach ($node->genericTypes as $index => $type) {
$variance = $node->variances[$index] ?? GenericTypeNode::VARIANCE_INVARIANT;
if ($variance === GenericTypeNode::VARIANCE_INVARIANT) {
$genericTypes[] = $this->printType($type);
} elseif ($variance === GenericTypeNode::VARIANCE_BIVARIANT) {
$genericTypes[] = '*';
} else {
$genericTypes[] = sprintf('%s %s', $variance, $this->print($type));
}
}
return $node->type . '<' . implode(', ', $genericTypes) . '>';
}
if ($node instanceof IdentifierTypeNode) {
return $node->name;
}
if ($node instanceof IntersectionTypeNode || $node instanceof UnionTypeNode) {
$items = [];
foreach ($node->types as $type) {
if (
$type instanceof IntersectionTypeNode
|| $type instanceof UnionTypeNode
|| $type instanceof NullableTypeNode
) {
$items[] = $this->wrapInParentheses($type);
continue;
}
$items[] = $this->printType($type);
}
return implode($node instanceof IntersectionTypeNode ? '&' : '|', $items);
}
if ($node instanceof InvalidTypeNode) {
return (string) $node;
}
if ($node instanceof NullableTypeNode) {
if ($node->type instanceof IntersectionTypeNode || $node->type instanceof UnionTypeNode) {
return '?(' . $this->printType($node->type) . ')';
}
return '?' . $this->printType($node->type);
}
if ($node instanceof ObjectShapeNode) {
$items = array_map(fn (ObjectShapeItemNode $item): string => $this->print($item), $node->items);
return 'object{' . implode(', ', $items) . '}';
}
if ($node instanceof OffsetAccessTypeNode) {
return $this->printOffsetAccessType($node->type) . '[' . $this->printType($node->offset) . ']';
}
if ($node instanceof ThisTypeNode) {
return (string) $node;
}
throw new LogicException(sprintf('Unknown node type %s', get_class($node)));
}
private function wrapInParentheses(TypeNode $node): string
{
return '(' . $this->printType($node) . ')';
}
private function printOffsetAccessType(TypeNode $type): string
{
if (
$type instanceof CallableTypeNode
|| $type instanceof UnionTypeNode
|| $type instanceof IntersectionTypeNode
|| $type instanceof NullableTypeNode
) {
return $this->wrapInParentheses($type);
}
return $this->printType($type);
}
private function printConstExpr(ConstExprNode $node): string
{
// this is fine - ConstExprNode classes do not contain nodes that need smart printer logic
return (string) $node;
}
/**
* @param Node[] $nodes
* @param Node[] $originalNodes
*/
private function printArrayFormatPreserving(array $nodes, array $originalNodes, TokenIterator $originalTokens, int &$tokenIndex, string $parentNodeClass, string $subNodeName): ?string
{
$diff = $this->differ->diffWithReplacements($originalNodes, $nodes);
$mapKey = $parentNodeClass . '->' . $subNodeName;
$insertStr = $this->listInsertionMap[$mapKey] ?? null;
$result = '';
$beforeFirstKeepOrReplace = true;
$delayedAdd = [];
$insertNewline = false;
[$isMultiline, $beforeAsteriskIndent, $afterAsteriskIndent] = $this->isMultiline($tokenIndex, $originalNodes, $originalTokens);
if ($insertStr === "\n * ") {
$insertStr = sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
}
foreach ($diff as $i => $diffElem) {
$diffType = $diffElem->type;
$arrItem = $diffElem->new;
$origArrayItem = $diffElem->old;
if ($diffType === DiffElem::TYPE_KEEP || $diffType === DiffElem::TYPE_REPLACE) {
$beforeFirstKeepOrReplace = false;
if (!$arrItem instanceof Node || !$origArrayItem instanceof Node) {
return null;
}
/** @var int $itemStartPos */
$itemStartPos = $origArrayItem->getAttribute(Attribute::START_INDEX);
/** @var int $itemEndPos */
$itemEndPos = $origArrayItem->getAttribute(Attribute::END_INDEX);
if ($itemStartPos < 0 || $itemEndPos < 0 || $itemStartPos < $tokenIndex) {
throw new LogicException();
}
$comments = $arrItem->getAttribute(Attribute::COMMENTS) ?? [];
$origComments = $origArrayItem->getAttribute(Attribute::COMMENTS) ?? [];
$commentStartPos = count($origComments) > 0 ? $origComments[0]->startIndex : $itemStartPos;
assert($commentStartPos >= 0);
$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 .= '(';
}
if ($insertNewline) {
$delayedAddComments = $delayedAddNode->getAttribute(Attribute::COMMENTS) ?? [];
if (count($delayedAddComments) > 0) {
$result .= $this->printComments($delayedAddComments, $beforeAsteriskIndent, $afterAsteriskIndent);
$result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
}
}
$result .= $this->printNodeFormatPreserving($delayedAddNode, $originalTokens);
if ($parenthesesNeeded) {
$result .= ')';
}
if ($insertNewline) {
$result .= $insertStr . sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
} else {
$result .= $insertStr;
}
}
$delayedAdd = [];
}
$parenthesesNeeded = isset($this->parenthesesListMap[$mapKey])
&& in_array(get_class($arrItem), $this->parenthesesListMap[$mapKey], true)
&& !in_array(get_class($origArrayItem), $this->parenthesesListMap[$mapKey], true);
$addParentheses = $parenthesesNeeded && !$originalTokens->hasParentheses($itemStartPos, $itemEndPos);
if ($addParentheses) {
$result .= '(';
}
if ($comments !== $origComments) {
if (count($comments) > 0) {
$result .= $this->printComments($comments, $beforeAsteriskIndent, $afterAsteriskIndent);
$result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
}
}
$result .= $this->printNodeFormatPreserving($arrItem, $originalTokens);
if ($addParentheses) {
$result .= ')';
}
$tokenIndex = $itemEndPos + 1;
} elseif ($diffType === DiffElem::TYPE_ADD) {
if ($insertStr === null) {
return null;
}
if (!$arrItem instanceof Node) {
return null;
}
if ($insertStr === ', ' && $isMultiline || count($arrItem->getAttribute(Attribute::COMMENTS) ?? []) > 0) {
$insertStr = ',';
$insertNewline = true;
}
if ($beforeFirstKeepOrReplace) {
// Will be inserted at the next "replace" or "keep" element
$delayedAdd[] = $arrItem;
continue;
}
/** @var int $itemEndPos */
$itemEndPos = $tokenIndex - 1;
if ($insertNewline) {
$comments = $arrItem->getAttribute(Attribute::COMMENTS) ?? [];
$result .= $insertStr;
if (count($comments) > 0) {
$result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
$result .= $this->printComments($comments, $beforeAsteriskIndent, $afterAsteriskIndent);
}
$result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
} else {
$result .= $insertStr;
}
$parenthesesNeeded = isset($this->parenthesesListMap[$mapKey])
&& in_array(get_class($arrItem), $this->parenthesesListMap[$mapKey], true);
if ($parenthesesNeeded) {
$result .= '(';
}
$result .= $this->printNodeFormatPreserving($arrItem, $originalTokens);
if ($parenthesesNeeded) {
$result .= ')';
}
$tokenIndex = $itemEndPos + 1;
} elseif ($diffType === DiffElem::TYPE_REMOVE) {
if (!$origArrayItem instanceof Node) {
return null;
}
/** @var int $itemStartPos */
$itemStartPos = $origArrayItem->getAttribute(Attribute::START_INDEX);
/** @var int $itemEndPos */
$itemEndPos = $origArrayItem->getAttribute(Attribute::END_INDEX);
if ($itemStartPos < 0 || $itemEndPos < 0) {
throw new LogicException();
}
if ($i === 0) {
// If we're removing from the start, keep the tokens before the node and drop those after it,
// instead of the other way around.
$originalTokensArray = $originalTokens->getTokens();
for ($j = $tokenIndex; $j < $itemStartPos; $j++) {
if ($originalTokensArray[$j][Lexer::TYPE_OFFSET] === Lexer::TOKEN_PHPDOC_EOL) {
break;
}
$result .= $originalTokensArray[$j][Lexer::VALUE_OFFSET];
}
}
$tokenIndex = $itemEndPos + 1;
}
}
if (count($delayedAdd) > 0) {
if (!isset($this->emptyListInsertionMap[$mapKey])) {
return null;
}
[$findToken, $extraLeft, $extraRight] = $this->emptyListInsertionMap[$mapKey];
if ($findToken !== null) {
$originalTokensArray = $originalTokens->getTokens();
for (; $tokenIndex < count($originalTokensArray); $tokenIndex++) {
$result .= $originalTokensArray[$tokenIndex][Lexer::VALUE_OFFSET];
if ($originalTokensArray[$tokenIndex][Lexer::VALUE_OFFSET] !== $findToken) {
continue;
}
$tokenIndex++;
break;
}
}
$first = true;
$result .= $extraLeft;
foreach ($delayedAdd as $delayedAddNode) {
if (!$first) {
$result .= $insertStr;
if ($insertNewline) {
$result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
}
}
$result .= $this->printNodeFormatPreserving($delayedAddNode, $originalTokens);
$first = false;
}
$result .= $extraRight;
}
return $result;
}
/**
* @param list<Comment> $comments
*/
private function printComments(array $comments, string $beforeAsteriskIndent, string $afterAsteriskIndent): string
{
$formattedComments = [];
foreach ($comments as $comment) {
$formattedComments[] = str_replace("\n", "\n" . $beforeAsteriskIndent . '*' . $afterAsteriskIndent, $comment->getReformattedText());
}
return implode("\n$beforeAsteriskIndent*$afterAsteriskIndent", $formattedComments);
}
/**
* @param array<Node|null> $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'];
}
$before = strlen($before) === 0 ? ' ' : $before;
$after = strlen($after) === 0 ? ' ' : $after;
return [$isMultiline, $before, $after];
}
private function printNodeFormatPreserving(Node $node, TokenIterator $originalTokens): string
{
/** @var Node|null $originalNode */
$originalNode = $node->getAttribute(Attribute::ORIGINAL_NODE);
if ($originalNode === null) {
return $this->print($node);
}
$class = get_class($node);
if ($class !== get_class($originalNode)) {
throw new LogicException();
}
$startPos = $originalNode->getAttribute(Attribute::START_INDEX);
$endPos = $originalNode->getAttribute(Attribute::END_INDEX);
if ($startPos < 0 || $endPos < 0) {
throw new LogicException();
}
$result = '';
$pos = $startPos;
$subNodeNames = array_keys(get_object_vars($node));
foreach ($subNodeNames as $subNodeName) {
$subNode = $node->$subNodeName;
$origSubNode = $originalNode->$subNodeName;
if (
(!$subNode instanceof Node && $subNode !== null)
|| (!$origSubNode instanceof Node && $origSubNode !== null)
) {
if ($subNode === $origSubNode) {
// Unchanged, can reuse old code
continue;
}
if (is_array($subNode) && is_array($origSubNode)) {
// Array subnode changed, we might be able to reconstruct it
$listResult = $this->printArrayFormatPreserving(
$subNode,
$origSubNode,
$originalTokens,
$pos,
$class,
$subNodeName,
);
if ($listResult === null) {
return $this->print($node);
}
$result .= $listResult;
continue;
}
return $this->print($node);
}
if ($origSubNode === null) {
if ($subNode === null) {
// Both null, nothing to do
continue;
}
return $this->print($node);
}
$subStartPos = $origSubNode->getAttribute(Attribute::START_INDEX);
$subEndPos = $origSubNode->getAttribute(Attribute::END_INDEX);
if ($subStartPos < 0 || $subEndPos < 0) {
throw new LogicException();
}
if ($subEndPos < $subStartPos) {
return $this->print($node);
}
if ($subNode === null) {
return $this->print($node);
}
$result .= $originalTokens->getContentBetween($pos, $subStartPos);
$mapKey = get_class($node) . '->' . $subNodeName;
$parenthesesNeeded = isset($this->parenthesesMap[$mapKey])
&& in_array(get_class($subNode), $this->parenthesesMap[$mapKey], true);
if ($subNode->getAttribute(Attribute::ORIGINAL_NODE) !== null) {
$parenthesesNeeded = $parenthesesNeeded
&& !in_array(get_class($subNode->getAttribute(Attribute::ORIGINAL_NODE)), $this->parenthesesMap[$mapKey], true);
}
$addParentheses = $parenthesesNeeded && !$originalTokens->hasParentheses($subStartPos, $subEndPos);
if ($addParentheses) {
$result .= '(';
}
$result .= $this->printNodeFormatPreserving($subNode, $originalTokens);
if ($addParentheses) {
$result .= ')';
}
$pos = $subEndPos + 1;
}
return $result . $originalTokens->getContentBetween($pos, $endPos + 1);
}
}
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2016 Ondřej Mirtes
Copyright (c) 2025 PHPStan s.r.o.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,118 @@
<h1 align="center">PHPStan - PHP Static Analysis Tool</h1>
<p align="center">
<img src="https://i.imgur.com/WaRKPlC.png" alt="PHPStan" width="300" height="300">
</p>
<p align="center">
<a href="https://github.com/phpstan/phpstan/actions"><img src="https://github.com/phpstan/phpstan/workflows/Tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/phpstan/phpstan"><img src="https://poser.pugx.org/phpstan/phpstan/v/stable" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/phpstan/phpstan/stats"><img src="https://poser.pugx.org/phpstan/phpstan/downloads" alt="Total Downloads"></a>
<a href="https://choosealicense.com/licenses/mit/"><img src="https://poser.pugx.org/phpstan/phpstan/license" alt="License"></a>
<a href="https://phpstan.org/"><img src="https://img.shields.io/badge/PHPStan-enabled-brightgreen.svg?style=flat" alt="PHPStan Enabled"></a>
</p>
------
PHPStan focuses on finding errors in your code without actually running it. It catches whole classes of bugs
even before you write tests for the code. It moves PHP closer to compiled languages in the sense that the correctness of each line of the code
can be checked before you run the actual line.
**[Read more about PHPStan »](https://phpstan.org/)**
**[Try out PHPStan on the on-line playground! »](https://phpstan.org/try)**
## Sponsors
Want your logo here? [Learn more »](https://phpstan.org/sponsor)
### Gold Sponsors
<a href="https://ma.tt/"><img src="website/src/images/sponsor/matt.png" alt="Matt Mullenweg" width="290" height="64"></a>
<a href="https://mojam.co/"><img src="website/src/images/sponsor/mojam.png" alt="Mojam" width="290" height="64"></a>
<br><br>
### Silver Sponsors
<a href="https://www.startupjobs.cz/startup/shipmonk"><img src="website/src/images/sponsor/shipmonk.jpg" alt="ShipMonk" width="290" height="64"></a>
<a href="https://www.shopware.com/en/"><img src="website/src/images/sponsor/shopware.png" alt="Shopware" width="284" height="64"></a>
<br><br>
### Bronze Sponsors
<a href="https://coders.thecodingmachine.com/phpstan"><img src="website/src/images/sponsor/tcm.png" alt="TheCodingMachine" width="247" height="64"></a>
&nbsp;&nbsp;&nbsp;
<a href="https://packagist.com/?utm_source=phpstan&utm_medium=readme&utm_campaign=sponsorlogo"><img src="website/src/images/sponsor/packagist.png" alt="Private Packagist" width="283" height="64"></a>
<br>
<a href="https://www.cdn77.com/"><img src="website/src/images/sponsor/cdn77.png" alt="CDN77" width="290" height="64"></a>
&nbsp;&nbsp;&nbsp;
<a href="https://blackfire.io/docs/introduction?utm_source=phpstan&utm_medium=github_readme&utm_campaign=logo"><img src="website/src/images/sponsor/blackfire.png" alt="Blackfire.io" width="254" height="64"></a>
<br>
<a href="https://www.iodigital.com/"><img src="website/src/images/sponsor/io.png" alt="iO" width="254" height="65"></a>
&nbsp;&nbsp;&nbsp;
<a href="https://www.fame.fi/"><img src="website/src/images/sponsor/fame.png" alt="Fame Helsinki" width="283" height="64"></a>
<br>
<a href="https://werkenbijbelsimpel.nl/en/about-us/"><img src="website/src/images/sponsor/belsimpel.png" alt="Belsimpel" width="284" height="64"></a>
&nbsp;&nbsp;&nbsp;
<a href="https://togetter.com/"><img src="website/src/images/sponsor/togetter.png" alt="Togetter" width="283" height="64"></a>
<br>
<a href="https://join.rightcapital.com/?utm_source=phpstan&utm_medium=github&utm_campaign=sponsorship"><img src="website/src/images/sponsor/rightcapital.png" alt="RightCapital" width="283" height="64"></a>
&nbsp;&nbsp;&nbsp;
<a href="https://www.shoptet.cz/"><img src="website/src/images/sponsor/shoptet.png" alt="Shoptet" width="283" height="64"></a>
<br>
<a href="https://zol.fr?utm_source=phpstan"><img src="website/src/images/sponsor/zol.png" alt="ZOL" width="283" height="64"></a>
&nbsp;&nbsp;&nbsp;
<a href="https://www.edgenext.com/"><img src="website/src/images/sponsor/edgenext.png" alt="EdgeNext" width="283" height="64"></a>
<br>
<a href="https://route4me.com/"><img src="website/src/images/sponsor/route4me.png" alt="Route4Me: Route Optimizer and Route Planner Software" width="283" height="64"></a>
&nbsp;&nbsp;&nbsp;
<a href="https://craftcms.com/"><img src="website/src/images/sponsor/craftcms.png" alt="Craft CMS" width="283" height="64"></a>
<br>
<a href="https://jobs.ticketswap.com/"><img src="website/src/images/sponsor/ticketswap.png" alt="TicketSwap" width="269" height="64"></a>
&nbsp;&nbsp;&nbsp;
<a href="https://www.campoint.net/"><img src="website/src/images/sponsor/campoint.png" alt="campoint AG" width="283" height="64"></a>
<br>
<a href="https://www.crisp.nl/"><img src="website/src/images/sponsor/crisp.png" alt="Crisp.nl" width="283" height="64"></a>
&nbsp;&nbsp;&nbsp;
<a href="https://inviqa.com/"><img src="website/src/images/sponsor/inviqa.png" alt="Inviqa" width="254" height="65"></a>
<br>
[**You can sponsor my open-source work on PHPStan through GitHub Sponsors and also directly.**](https://phpstan.org/sponsor)
One-time donations [through Revolut.me](https://revolut.me/ondrejmirtes) are also accepted. To request an invoice, [contact me](mailto:ondrej@mirtes.cz) through e-mail.
## Documentation
All the documentation lives on the [phpstan.org website](https://phpstan.org/):
* [Getting Started & User Guide](https://phpstan.org/user-guide/getting-started)
* [Config Reference](https://phpstan.org/config-reference)
* [PHPDocs Basics](https://phpstan.org/writing-php-code/phpdocs-basics) & [PHPDoc Types](https://phpstan.org/writing-php-code/phpdoc-types)
* [Extension Library](https://phpstan.org/user-guide/extension-library)
* [Developing Extensions](https://phpstan.org/developing-extensions/extension-types)
* [API Reference](https://apiref.phpstan.org/)
## PHPStan Pro
PHPStan Pro is a paid add-on on top of open-source PHPStan Static Analysis Tool with these premium features:
* Web UI for browsing found errors, you can click and open your editor of choice on the offending line.
* Continuous analysis (watch mode): scans changed files in the background, refreshes the UI automatically.
Try it on PHPStan 0.12.45 or later by running it with the `--pro` option. You can create an account either by following the on-screen instructions, or by visiting [account.phpstan.com](https://account.phpstan.com/).
After 30-day free trial period it costs 7 EUR for individuals monthly, 70 EUR for teams (up to 25 members). By paying for PHPStan Pro, you're supporting the development of open-source PHPStan.
You can read more about it on [PHPStan's website](https://phpstan.org/blog/introducing-phpstan-pro).
## Code of Conduct
This project adheres to a [Contributor Code of Conduct](https://github.com/phpstan/phpstan/blob/master/CODE_OF_CONDUCT.md). By participating in this project and its community, you are expected to uphold this code.
## Contributing
Any contributions are welcome. PHPStan's source code open to pull requests lives at [`phpstan/phpstan-src`](https://github.com/phpstan/phpstan-src).
@@ -0,0 +1,338 @@
Upgrading from PHPStan 1.x to 2.0
=================================
## PHP version requirements
PHPStan now requires PHP 7.4 or newer to run.
## Upgrading guide for end users
The best way to get ready for upgrade to PHPStan 2.0 is to update to the **latest PHPStan 1.12 release**
and enable [**Bleeding Edge**](https://phpstan.org/blog/what-is-bleeding-edge). This will enable the new rules and behaviours that 2.0 turns on for all users.
Also make sure to install and enable [`phpstan/phpstan-deprecation-rules`](https://github.com/phpstan/phpstan-deprecation-rules).
Once you get to a green build with no deprecations showed on latest PHPStan 1.12.x with Bleeding Edge enabled, you can update all your related PHPStan dependencies to 2.0 in `composer.json`:
```json
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpstan/phpstan-doctrine": "^2.0",
"phpstan/phpstan-nette": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpstan/phpstan-symfony": "^2.0",
"phpstan/phpstan-webmozart-assert": "^2.0",
...
}
```
Don't forget to update [3rd party PHPStan extensions](https://phpstan.org/user-guide/extension-library) as well.
After changing your `composer.json`, run `composer update 'phpstan/*' -W`.
It's up to you whether you go through the new reported errors or if you just put them all to the [baseline](https://phpstan.org/user-guide/baseline) ;) Everyone who's on PHPStan 1.12 should be able to upgrade to PHPStan 2.0.
### Noteworthy changes to code analysis
* [**Enhancements in handling parameters passed by reference**](https://phpstan.org/blog/enhancements-in-handling-parameters-passed-by-reference)
* [**Validate inline PHPDoc `@var` tag type**](https://phpstan.org/blog/phpstan-1-10-comes-with-lie-detector#validate-inline-phpdoc-%40var-tag-type)
* [**List type enforced**](https://phpstan.org/blog/phpstan-1-9-0-with-phpdoc-asserts-list-type#list-type)
* **Always `true` conditions always reported**: previously reported only with phpstan-strict-rules, this is now always reported.
### Removed option `checkMissingIterableValueType`
It's strongly recommended to add the missing array typehints.
If you want to continue ignoring missing typehints from arrays, add `missingType.iterableValue` error identifier to your `ignoreErrors`:
```neon
parameters:
ignoreErrors:
-
identifier: missingType.iterableValue
```
### Removed option `checkGenericClassInNonGenericObjectType`
It's strongly recommended to add the missing generic typehints.
If you want to continue ignoring missing typehints from generics, add `missingType.generics` error identifier to your `ignoreErrors`:
```neon
parameters:
ignoreErrors:
-
identifier: missingType.generics
```
### Removed `checkAlwaysTrue*` options
These options have been removed because PHPStan now always behaves as if these were set to `true`:
* `checkAlwaysTrueCheckTypeFunctionCall`
* `checkAlwaysTrueInstanceof`
* `checkAlwaysTrueStrictComparison`
* `checkAlwaysTrueLooseComparison`
### Removed option `excludes_analyse`
It has been replaced with [`excludePaths`](https://phpstan.org/user-guide/ignoring-errors#excluding-whole-files).
### Paths in `excludePaths` and `ignoreErrors` have to be a valid file path or a fnmatch pattern
If you are excluding a file path that might not exist but you still want to have it in `excludePaths`, append `(?)`:
```neon
parameters:
excludePaths:
- tests/*/data/*
- src/broken
- node_modules (?) # optional path, might not exist
```
If you have the same situation in `ignoreErrors` (ignoring an error in a path that might not exist), use `reportUnmatchedIgnoredErrors: false`.
```neon
parameters:
reportUnmatchedIgnoredErrors: false
```
Appending `(?)` in `ignoreErrors` is not supported.
### Changes in 1st party PHPStan extensions
* [phpstan-doctrine](https://github.com/phpstan/phpstan-doctrine)
* Removed config parameter `searchOtherMethodsForQueryBuilderBeginning` (extension now behaves as when this was set to `true`)
* Removed config parameter `queryBuilderFastAlgorithm` (extension now behaves as when this was set to `false`)
* [phpstan-symfony](https://github.com/phpstan/phpstan-symfony)
* Removed legacy options with `_` in the name
* `container_xml_path` -> use `containerXmlPath`
* `constant_hassers` -> use `constantHassers`
* `console_application_loader` -> use `consoleApplicationLoader`
### Minor backward compatibility breaks
* Removed unused config parameter `cache.nodesByFileCountMax`
* Removed unused config parameter `memoryLimitFile`
* Removed unused feature toggle `disableRuntimeReflectionProvider`
* Removed unused config parameter `staticReflectionClassNamePatterns`
* Remove `fixerTmpDir` config parameter, use `pro.tmpDir` instead
* Remove `tempResultCachePath` config parameter, use `resultCachePath` instead
* `additionalConfigFiles` config parameter must be a list
## Upgrading guide for extension developers
> [!NOTE]
> Please switch to PHPStan 2.0 in a new major version of your extension. It's not feasible to try to support both PHPStan 1.x and PHPStan 2.x with the same extension code.
>
> You can definitely get closer to supporting PHPStan 2.0 without increasing major version by solving reported deprecations and other issues by analysing your extension code with PHPStan & phpstan-deprecation-rules & Bleeding Edge, but the final leap and solving backward incompatibilities should be done by requiring `"phpstan/phpstan": "^2.0"` in your `composer.json`, and releasing a new major version.
### PHPStan now uses nikic/php-parser v5
See [UPGRADING](https://github.com/nikic/PHP-Parser/blob/master/UPGRADE-5.0.md) guide for PHP-Parser.
The most notable change is how `throw` statement is represented. Previously, `throw` statements like `throw $e;` were represented using the `Stmt\Throw_` class, while uses inside other expressions (such as `$x ?? throw $e`) used the `Expr\Throw_` class.
Now, `throw $e;` is represented as a `Stmt\Expression` that contains an `Expr\Throw_`. The
`Stmt\Throw_` class has been removed.
### PHPStan now uses phpstan/phpdoc-parser v2
See [UPGRADING](https://github.com/phpstan/phpdoc-parser/blob/2.0.x/UPGRADING.md) guide for phpstan/phpdoc-parser.
### Returning plain strings as errors no longer supported, use RuleErrorBuilder
Identifiers are also required in custom rules.
Learn more: [Using RuleErrorBuilder to enrich reported errors in custom rules](https://phpstan.org/blog/using-rule-error-builder)
**Before**:
```php
return ['My error'];
```
**After**:
```php
return [
RuleErrorBuilder::message('My error')
->identifier('my.error')
->build(),
];
```
### Deprecate various `instanceof *Type` in favour of new methods on `Type` interface
Learn more: [Why Is instanceof *Type Wrong and Getting Deprecated?](https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated)
### Removed deprecated `ParametersAcceptorSelector::selectSingle()`
Use [`ParametersAcceptorSelector::selectFromArgs()`](https://apiref.phpstan.org/2.0.x/PHPStan.Reflection.ParametersAcceptorSelector.html#_selectFromArgs) instead. It should be used in most places where `selectSingle()` was previously used, like dynamic return type extensions.
**Before**:
```php
$defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
```
**After**:
```php
$defaultReturnType = ParametersAcceptorSelector::selectFromArgs(
$scope,
$functionCall->getArgs(),
$functionReflection->getVariants()
)->getReturnType();
```
If you're analysing function or method body itself and you're using one of the following methods, ask for `getParameters()` and `getReturnType()` directly on the reflection object:
* [InClassMethodNode::getMethodReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.InClassMethodNode.html)
* [InFunctionNode::getFunctionReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.InFunctionNode.html)
* [FunctionReturnStatementsNode::getFunctionReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.FunctionReturnStatementsNode.html)
* [MethodReturnStatementsNode::getMethodReflection()](https://apiref.phpstan.org/2.0.x/PHPStan.Node.MethodReturnStatementsNode.html)
* [Scope::getFunction()](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.Scope.html#_getFunction)
**Before**:
```php
$function = $node->getFunctionReflection();
$returnType = ParametersAcceptorSelector::selectSingle($function->getVariants())->getReturnType();
```
**After**:
```php
$returnType = $node->getFunctionReflection()->getReturnType();
```
### Changed `TypeSpecifier::create()` and `SpecifiedTypes` constructor parameters
[`PHPStan\Analyser\TypeSpecifier::create()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.TypeSpecifier.html#_create) now accepts (all parameters are required):
* `Expr $expr`
* `Type $type`
* `TypeSpecifierContext $context`
* `Scope $scope`
If you want to change `$overwrite` or `$rootExpr` (previous parameters also used to be accepted by this method), call `setAlwaysOverwriteTypes()` and `setRootExpr()` on [`SpecifiedTypes`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.SpecifiedTypes.html) (object returned by `TypeSpecifier::create()`). These methods return a new object (SpecifiedTypes is immutable).
[`SpecifiedTypes`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.SpecifiedTypes.html) constructor now accepts:
* `array $sureTypes`
* `array $sureNotTypes`
If you want to change `$overwrite` or `$rootExpr` (previous parameters also used to be accepted by the constructor), call `setAlwaysOverwriteTypes()` and `setRootExpr()`. These methods return a new object (SpecifiedTypes is immutable).
### `ConstantArrayType` no longer extends `ArrayType`
`Type::getArrays()` now returns `list<ArrayType|ConstantArrayType>`.
Using `$type instanceof ArrayType` is [being deprecated anyway](https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated) so the impact of this change should be minimal.
### Changed `TypeSpecifier::specifyTypesInCondition()`
This method no longer accepts `Expr $rootExpr`. If you want to change it, call `setRootExpr()` on [`SpecifiedTypes`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.SpecifiedTypes.html) (object returned by `TypeSpecifier::specifyTypesInCondition()`). `setRootExpr()` method returns a new object (SpecifiedTypes is immutable).
### Node attributes `parent`, `previous`, `next` are no longer available
Learn more: https://phpstan.org/blog/preprocessing-ast-for-custom-rules
### Removed config parameter `scopeClass`
As a replacement you can implement [`PHPStan\Type\ExpressionTypeResolverExtension`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.ExpressionTypeResolverExtension.html) interface instead and register it as a service.
### Removed `PHPStan\Broker\Broker`
Use [`PHPStan\Reflection\ReflectionProvider`](https://apiref.phpstan.org/2.0.x/PHPStan.Reflection.ReflectionProvider.html) instead.
`BrokerAwareExtension` was also removed. Ask for `ReflectionProvider` in the extension constructor instead.
Instead of `PHPStanTestCase::createBroker()`, call `PHPStanTestCase::createReflectionProvider()`.
### List type is enabled for everyone
Removed static methods from `AccessoryArrayListType` class:
* `isListTypeEnabled()`
* `setListTypeEnabled()`
* `intersectWith()`
Instead of `AccessoryArrayListType::intersectWith($type)`, do `TypeCombinator::intersect($type, new AccessoryArrayListType())`.
### Minor backward compatibility breaks
* Classes that were previously `@final` were made `final`
* Parameter `$callableParameters` of [`MutatingScope::enterAnonymousFunction()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.MutatingScope.html#_enterAnonymousFunction) and [`enterArrowFunction()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.MutatingScope.html#_enterArrowFunction) made required
* Parameter `StatementContext $context` of [`NodeScopeResolver::processStmtNodes()`](https://apiref.phpstan.org/2.0.x/PHPStan.Analyser.NodeScopeResolver.html#_processStmtNodes) made required
* ClassPropertiesNode - remove `$extensions` parameter from [`getUninitializedProperties()`](https://apiref.phpstan.org/2.0.x/PHPStan.Node.ClassPropertiesNode.html#_getUninitializedProperties)
* `Type::getSmallerType()`, `Type::getSmallerOrEqualType()`, `Type::getGreaterType()`, `Type::getGreaterOrEqualType()`, `Type::isSmallerThan()`, `Type::isSmallerThanOrEqual()` now require [`PhpVersion`](https://apiref.phpstan.org/2.0.x/PHPStan.Php.PhpVersion.html) as argument.
* `CompoundType::isGreaterThan()`, `CompoundType::isGreaterThanOrEqual()` now require [`PhpVersion`](https://apiref.phpstan.org/2.0.x/PHPStan.Php.PhpVersion.html) as argument.
* Removed `ReflectionProvider::supportsAnonymousClasses()` (all reflection providers support anonymous classes)
* Remove `ArrayType::generalizeKeys()`
* Remove `ArrayType::count()`, use `Type::getArraySize()` instead
* Remove `ArrayType::castToArrayKeyType()`, `Type::toArrayKey()` instead
* Remove `UnionType::pickTypes()`, use `pickFromTypes()` instead
* Remove `RegexArrayShapeMatcher::matchType()`, use `matchExpr()` instead
* Remove unused `PHPStanTestCase::$useStaticReflectionProvider`
* Remove `PHPStanTestCase::getReflectors()`, use `getReflector()` instead
* Remove `ClassReflection::getFileNameWithPhpDocs()`, use `getFileName()` instead
* Remove `AnalysisResult::getInternalErrors()`, use `getInternalErrorObjects()` instead
* Remove `ConstantReflection::getValue()`, use `getValueExpr()` instead. To get `Type` from `Expr`, use `Scope::getType()` or `InitializerExprTypeResolver::getType()`
* Remove `PropertyTag::getType()`, use `getReadableType()` / `getWritableType()` instead
* Remove `GenericTypeVariableResolver`, use [`Type::getTemplateType()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getTemplateType) instead
* Rename `Type::isClassStringType()` to `Type::isClassString()`
* Remove `Scope::isSpecified()`, use `hasExpressionType()` instead
* Remove `ConstantArrayType::isEmpty()`, use `isIterableAtLeastOnce()->no()` instead
* Remove `ConstantArrayType::getNextAutoIndex()`
* Removed methods from `ConstantArrayType` - `getFirst*Type` and `getLast*Type`
* Use `getFirstIterable*Type` and `getLastIterable*Type` instead
* Remove `ConstantArrayType::generalizeToArray()`
* Remove `ConstantArrayType::findTypeAndMethodName()`, use `findTypeAndMethodNames()` instead
* Remove `ConstantArrayType::removeLast()`, use [`Type::popArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_popArray) instead
* Remove `ConstantArrayType::removeFirst()`, use [`Type::shiftArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_shiftArray) instead
* Remove `ConstantArrayType::reverse()`, use [`Type::reverseArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_reverseArray) instead
* Remove `ConstantArrayType::chunk()`, use [`Type::chunkArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_chunkArray) instead
* Remove `ConstantArrayType::slice()`, use [`Type::sliceArray()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_sliceArray) instead
* Made `TypeUtils` thinner by removing methods:
* Remove `TypeUtils::getArrays()` and `getAnyArrays()`, use [`Type::getArrays()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getArrays) instead
* Remove `TypeUtils::getConstantArrays()` and `getOldConstantArrays()`, use [`Type::getConstantArrays()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getConstantArrays) instead
* Remove `TypeUtils::getConstantStrings()`, use [`Type::getConstantStrings()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getConstantStrings) instead
* Remove `TypeUtils::getConstantTypes()` and `getAnyConstantTypes()`, use [`Type::isConstantValue()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isConstantValue) or [`Type::generalize()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_generalize)
* Remove `TypeUtils::generalizeType()`, use [`Type::generalize()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_generalize) instead
* Remove `TypeUtils::getDirectClassNames()`, use [`Type::getObjectClassNames()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getObjectClassNames) instead
* Remove `TypeUtils::getConstantScalars()`, use [`Type::isConstantScalarValue()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isConstantScalarValue) or [`Type::getConstantScalarTypes()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getConstantScalarTypes) instead
* Remove `TypeUtils::getEnumCaseObjects()`, use [`Type::getEnumCases()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getEnumCases) instead
* Remove `TypeUtils::containsCallable()`, use [`Type::isCallable()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isCallable) instead
* Removed `Scope::doNotTreatPhpDocTypesAsCertain()`, use `getNativeType()` instead
* Parameter `$isList` in `ConstantArrayType` constructor can only be `TrinaryLogic`, no longer `bool`
* Parameter `$nextAutoIndexes` in `ConstantArrayType` constructor can only be `non-empty-list<int>`, no longer `int`
* Remove `ConstantType` interface, use [`Type::isConstantValue()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_isConstantValue) instead
* `acceptsNamedArguments()` in `FunctionReflection`, `ExtendedMethodReflection` and `CallableParametersAcceptor` interfaces returns `TrinaryLogic` instead of `bool`
* Remove `FunctionReflection::isFinal()`
* [`Type::getProperty()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.Type.html#_getProperty) now returns [`ExtendedPropertyReflection`](https://apiref.phpstan.org/2.0.x/PHPStan.Reflection.ExtendedPropertyReflection.html)
* Remove `__set_state()` on objects that should not be serialized in cache
* Parameter `$selfClass` of [`TypehintHelper::decideTypeFromReflection()`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.TypehintHelper.html#_decideTypeFromReflection) no longer accepts `string`
* `LevelsTestCase::dataTopics()` data provider made static
* `PHPStan\Node\Printer\Printer` no longer autowired as `PhpParser\PrettyPrinter\Standard`, use `PHPStan\Node\Printer\Printer` in the typehint
* Remove `Type::acceptsWithReason()`, `Type:accepts()` return type changed from `TrinaryLogic` to [`AcceptsResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.AcceptsResult.html)
* Remove `CompoundType::isAcceptedWithReasonBy()`, `CompoundType::isAcceptedBy()` return type changed from `TrinaryLogic` to [`AcceptsResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.AcceptsResult.html)
Remove `Type::isSuperTypeOfWithReason()`, `Type:isSuperTypeOf()` return type changed from `TrinaryLogic` to [`IsSuperTypeOfResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.IsSuperTypeOfResult.html)
* Remove `CompoundType::isSubTypeOfWithReasonBy()`, `CompoundType::isSubTypeOf()` return type changed from `TrinaryLogic` to [`IsSuperTypeOfResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.IsSuperTypeOfResult.html)
* Remove `TemplateType::isValidVarianceWithReason()`, changed `TemplateType::isValidVariance()` return type to [`IsSuperTypeOfResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.IsSuperTypeOfResult.html)
* `RuleLevelHelper::accepts()` return type changed from `bool` to [`RuleLevelHelperAcceptsResult`](https://apiref.phpstan.org/2.0.x/PHPStan.Type.AcceptsResult.html)
* Changes around `ClassConstantReflection`
* Class `ClassConstantReflection` removed from BC promise, renamed to `RealClassConstantReflection`
* Interface `ConstantReflection` renamed to `ClassConstantReflection`
* Added more methods around PHPDoc types and native types to the (new) `ClassConstantReflection`
* Interface `GlobalConstantReflection` renamed to `ConstantReflection`
* Renamed interfaces and classes from `*WithPhpDocs` to `Extended*`
* `ParametersAcceptorWithPhpDocs` -> `ExtendedParametersAcceptor`
* `ParameterReflectionWithPhpDocs` -> `ExtendedParameterReflection`
* `FunctionVariantWithPhpDocs` -> `ExtendedFunctionVariant`
* `ClassPropertyNode::getNativeType()` return type changed from AST node to `Type|null`
* Class `PHPStan\Node\ClassMethod` (accessible from `ClassMethodsNode`) is no longer an AST node
* Call `PHPStan\Node\ClassMethod::getNode()` to access the original AST node
@@ -0,0 +1,144 @@
<?php declare(strict_types = 1);
namespace PHPStan;
use Composer\Autoload\ClassLoader;
use function class_exists;
use const PHP_VERSION_ID;
final class PharAutoloader
{
/** @var ClassLoader */
private static $composerAutoloader;
/** @var bool */
private static $polyfillsLoaded = false;
final public static function loadClass(string $class): void {
if (!extension_loaded('phar') || defined('__PHPSTAN_RUNNING__')) {
return;
}
if (strpos($class, '_PHPStan_') === 0) {
if (!in_array('phar', stream_get_wrappers(), true)) {
throw new \Exception('Phar wrapper is not registered. Please review your php.ini settings.');
}
if (self::$composerAutoloader === null) {
self::$composerAutoloader = require 'phar://' . __DIR__ . '/phpstan.phar/vendor/autoload.php';
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/jetbrains/phpstorm-stubs/PhpStormStubsMap.php';
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/react/async/src/functions_include.php';
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/react/promise/src/functions_include.php';
}
self::$composerAutoloader->loadClass($class);
return;
}
if (strpos($class, 'PHPStan\\') !== 0 || strpos($class, 'PHPStan\\PhpDocParser\\') === 0) {
return;
}
if (!in_array('phar', stream_get_wrappers(), true)) {
throw new \Exception('Phar wrapper is not registered. Please review your php.ini settings.');
}
if (!self::$polyfillsLoaded) {
self::$polyfillsLoaded = true;
if (
PHP_VERSION_ID < 80000
&& empty($GLOBALS['__composer_autoload_files']['a4a119a56e50fbb293281d9a48007e0e'])
&& !class_exists(\Symfony\Polyfill\Php80\Php80::class, false)
) {
$GLOBALS['__composer_autoload_files']['a4a119a56e50fbb293281d9a48007e0e'] = true;
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php80/Php80.php';
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php80/bootstrap.php';
}
if (
empty($GLOBALS['__composer_autoload_files']['0e6d7bf4a5811bfa5cf40c5ccd6fae6a'])
&& !class_exists(\Symfony\Polyfill\Mbstring\Mbstring::class, false)
) {
$GLOBALS['__composer_autoload_files']['0e6d7bf4a5811bfa5cf40c5ccd6fae6a'] = true;
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-mbstring/Mbstring.php';
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-mbstring/bootstrap.php';
}
if (
empty($GLOBALS['__composer_autoload_files']['e69f7f6ee287b969198c3c9d6777bd38'])
&& !class_exists(\Symfony\Polyfill\Intl\Normalizer\Normalizer::class, false)
) {
$GLOBALS['__composer_autoload_files']['e69f7f6ee287b969198c3c9d6777bd38'] = true;
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-intl-normalizer/Normalizer.php';
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-intl-normalizer/bootstrap.php';
}
if (
!extension_loaded('intl')
&& empty($GLOBALS['__composer_autoload_files']['8825ede83f2f289127722d4e842cf7e8'])
&& !class_exists(\Symfony\Polyfill\Intl\Grapheme\Grapheme::class, false)
) {
$GLOBALS['__composer_autoload_files']['8825ede83f2f289127722d4e842cf7e8'] = true;
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-intl-grapheme/Grapheme.php';
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-intl-grapheme/bootstrap.php';
}
if (
PHP_VERSION_ID < 80100
&& empty ($GLOBALS['__composer_autoload_files']['23c18046f52bef3eea034657bafda50f'])
&& !class_exists(\Symfony\Polyfill\Php81\Php81::class, false)
) {
$GLOBALS['__composer_autoload_files']['23c18046f52bef3eea034657bafda50f'] = true;
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php81/Php81.php';
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php81/bootstrap.php';
}
if (
PHP_VERSION_ID < 80300
&& empty ($GLOBALS['__composer_autoload_files']['662a729f963d39afe703c9d9b7ab4a8c'])
&& !class_exists(\Symfony\Polyfill\Php83\Php83::class, false)
) {
$GLOBALS['__composer_autoload_files']['662a729f963d39afe703c9d9b7ab4a8c'] = true;
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php83/Php83.php';
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php83/bootstrap.php';
}
if (
PHP_VERSION_ID < 80400
&& empty ($GLOBALS['__composer_autoload_files']['9d2b9fc6db0f153a0a149fefb182415e'])
&& !class_exists(\Symfony\Polyfill\Php83\Php84::class, false)
) {
$GLOBALS['__composer_autoload_files']['9d2b9fc6db0f153a0a149fefb182415e'] = true;
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php84/Php84.php';
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php84/bootstrap.php';
}
if (
PHP_VERSION_ID < 80500
&& empty ($GLOBALS['__composer_autoload_files']['606a39d89246991a373564698c2d8383'])
&& !class_exists(\Symfony\Polyfill\Php83\Php85::class, false)
) {
$GLOBALS['__composer_autoload_files']['606a39d89246991a373564698c2d8383'] = true;
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php85/Php85.php';
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php85/bootstrap.php';
}
}
$filename = str_replace('\\', DIRECTORY_SEPARATOR, $class);
if (strpos($class, 'PHPStan\\BetterReflection\\') === 0) {
$filename = substr($filename, strlen('PHPStan\\BetterReflection\\'));
$filepath = 'phar://' . __DIR__ . '/phpstan.phar/vendor/ondrejmirtes/better-reflection/src/' . $filename . '.php';
} else {
$filename = substr($filename, strlen('PHPStan\\'));
$filepath = 'phar://' . __DIR__ . '/phpstan.phar/src/' . $filename . '.php';
}
if (!file_exists($filepath)) {
return;
}
require $filepath;
}
}
spl_autoload_register([PharAutoloader::class, 'loadClass']);
@@ -0,0 +1,31 @@
{
"name": "phpstan/phpstan",
"description": "PHPStan - PHP Static Analysis Tool",
"license": ["MIT"],
"keywords": ["dev", "static analysis"],
"require": {
"php": "^7.4|^8.0"
},
"conflict": {
"phpstan/phpstan-shim": "*"
},
"bin": [
"phpstan",
"phpstan.phar"
],
"autoload": {
"files": ["bootstrap.php"]
},
"source": {
"type": "",
"url": "",
"reference": ""
},
"support": {
"issues": "https://github.com/phpstan/phpstan/issues",
"forum": "https://github.com/phpstan/phpstan/discussions",
"source": "https://github.com/phpstan/phpstan-src",
"docs": "https://phpstan.org/user-guide/getting-started",
"security": "https://github.com/phpstan/phpstan/security/policy"
}
}
@@ -0,0 +1,2 @@
includes:
- phar://phpstan.phar/conf/bleedingEdge.neon

Some files were not shown because too many files have changed in this diff Show More