phpcs.xml.dist 0000666 00000000766 13617754202 0007371 0 ustar 00
The coding standard for phpDocumentor.
src
tests/unit
*/tests/unit/Types/ContextFactoryTest.php
*/src/*/Abstract*.php
composer-require-config.json 0000666 00000000475 13617754202 0012224 0 ustar 00 {
"symbol-whitelist" : [
"null", "true", "false",
"static", "self", "parent",
"array", "string", "int", "float", "bool", "iterable", "callable", "void", "object", "XSLTProcessor"
],
"php-core-extensions" : [
"Core",
"pcre",
"Reflection",
"tokenizer",
"SPL",
"standard"
]
}
LICENSE 0000666 00000002070 13617754202 0005563 0 ustar 00 The MIT License (MIT)
Copyright (c) 2010 Mike van Riel
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.
psalm.xml 0000666 00000001021 13617754202 0006407 0 ustar 00
README.md 0000666 00000006015 13617754202 0006040 0 ustar 00 [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
![Qa workflow](https://github.com/phpDocumentor/ReflectionDocBlock/workflows/Qa%20workflow/badge.svg)
[![Coveralls Coverage](https://img.shields.io/coveralls/github/phpDocumentor/ReflectionDocBlock.svg)](https://coveralls.io/github/phpDocumentor/ReflectionDocBlock?branch=master)
[![Scrutinizer Code Coverage](https://img.shields.io/scrutinizer/coverage/g/phpDocumentor/ReflectionDocBlock.svg)](https://scrutinizer-ci.com/g/phpDocumentor/ReflectionDocBlock/?branch=master)
[![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/phpDocumentor/ReflectionDocBlock.svg)](https://scrutinizer-ci.com/g/phpDocumentor/ReflectionDocBlock/?branch=master)
[![Stable Version](https://img.shields.io/packagist/v/phpdocumentor/reflection-docblock.svg)](https://packagist.org/packages/phpdocumentor/reflection-docblock)
[![Unstable Version](https://img.shields.io/packagist/vpre/phpdocumentor/reflection-docblock.svg)](https://packagist.org/packages/phpdocumentor/reflection-docblock)
ReflectionDocBlock
==================
Introduction
------------
The ReflectionDocBlock component of phpDocumentor provides a DocBlock parser
that is 100% compatible with the [PHPDoc standard](http://phpdoc.org/docs/latest).
With this component, a library can provide support for annotations via DocBlocks
or otherwise retrieve information that is embedded in a DocBlock.
Installation
------------
```bash
composer require phpdocumentor/reflection-docblock
```
Usage
-----
In order to parse the DocBlock one needs a DocBlockFactory that can be
instantiated using its `createInstance` factory method like this:
```php
$factory = \phpDocumentor\Reflection\DocBlockFactory::createInstance();
```
Then we can use the `create` method of the factory to interpret the DocBlock.
Please note that it is also possible to provide a class that has the
`getDocComment()` method, such as an object of type `ReflectionClass`, the
create method will read that if it exists.
```php
$docComment = <<create($docComment);
```
The `create` method will yield an object of type `\phpDocumentor\Reflection\DocBlock`
whose methods can be queried:
```php
// Contains the summary for this DocBlock
$summary = $docblock->getSummary();
// Contains \phpDocumentor\Reflection\DocBlock\Description object
$description = $docblock->getDescription();
// You can either cast it to string
$description = (string) $docblock->getDescription();
// Or use the render method to get a string representation of the Description.
$description = $docblock->getDescription()->render();
```
> For more examples it would be best to review the scripts in the [`/examples` folder](/examples).
.github/workflows/push.yml 0000666 00000013044 13617754202 0011660 0 ustar 00 on: push
name: Qa workflow
jobs:
setup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Restore/cache vendor folder
uses: actions/cache@v1
with:
path: vendor
key: all-build-${{ hashFiles('**/composer.lock') }}
restore-keys: |
all-build-${{ hashFiles('**/composer.lock') }}
all-build-
- name: Restore/cache tools folder
uses: actions/cache@v1
with:
path: tools
key: all-tools-${{ github.sha }}
restore-keys: |
all-tools-${{ github.sha }}-
all-tools-
- name: composer
uses: docker://composer
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: install --no-interaction --prefer-dist --optimize-autoloader
- name: composer-require-checker
uses: docker://phpga/composer-require-checker-ga
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: check --config-file ./composer-require-config.json composer.json
- name: Install phive
run: make install-phive
- name: Install PHAR dependencies
run: tools/phive.phar --no-progress install --copy --trust-gpg-keys 4AA394086372C20A,D2CCAC42F6295E7D,E82B2FB314E9906E,8E730BA25823D8B5 --force-accept-unsigned
phpunit-with-coverage:
runs-on: ubuntu-latest
name: Unit tests
needs: setup
steps:
- uses: actions/checkout@master
- name: Restore/cache vendor folder
uses: actions/cache@v1
with:
path: vendor
key: all-build-${{ hashFiles('**/composer.lock') }}
restore-keys: |
all-build-${{ hashFiles('**/composer.lock') }}
all-build-
- name: Restore/cache tools folder
uses: actions/cache@v1
with:
path: tools
key: all-tools-${{ github.sha }}
restore-keys: |
all-tools-${{ github.sha }}-
all-tools-
- name: Setup PHP
uses: shivammathur/setup-php@master
with:
php-version: 7.2
extension-csv: mbstring, intl, iconv, libxml, dom, json, simplexml, zlib
ini-values-csv: memory_limit=2G, display_errors=On, error_reporting=-1
coverage: xdebug
pecl: false
- name: Run PHPUnit
run: php tools/phpunit
phpunit:
runs-on: ${{ matrix.operating-system }}
strategy:
matrix:
operating-system:
- ubuntu-latest
- windows-latest
- macOS-latest
php-versions: ['7.2', '7.3', '7.4']
name: Unit tests for PHP version ${{ matrix.php-versions }} on ${{ matrix.operating-system }}
needs:
- setup
- phpunit-with-coverage
steps:
- uses: actions/checkout@master
- name: Restore/cache vendor folder
uses: actions/cache@v1
with:
path: vendor
key: all-build-${{ hashFiles('**/composer.lock') }}
restore-keys: |
all-build-${{ hashFiles('**/composer.lock') }}
all-build-
- name: Restore/cache tools folder
uses: actions/cache@v1
with:
path: tools
key: all-tools-${{ github.sha }}
restore-keys: |
all-tools-${{ github.sha }}-
all-tools-
- name: Setup PHP
uses: shivammathur/setup-php@master
with:
php-version: ${{ matrix.php-versions }}
extension-csv: mbstring, intl, iconv, libxml, dom, json, simplexml, zlib
ini-values-csv: memory_limit=2G, display_errors=On, error_reporting=-1
pecl: false
- name: Run PHPUnit
continue-on-error: true
run: php tools/phpunit
codestyle:
runs-on: ubuntu-latest
needs: [setup, phpunit]
steps:
- uses: actions/checkout@master
- name: Restore/cache vendor folder
uses: actions/cache@v1
with:
path: vendor
key: all-build-${{ hashFiles('**/composer.lock') }}
restore-keys: |
all-build-${{ hashFiles('**/composer.lock') }}
all-build-
- name: Code style check
uses: phpDocumentor/coding-standard@v1.0.0
with:
args: -s
phpstan:
runs-on: ubuntu-latest
needs: [setup, phpunit]
steps:
- uses: actions/checkout@master
- name: Restore/cache vendor folder
uses: actions/cache@v1
with:
path: vendor
key: all-build-${{ hashFiles('**/composer.lock') }}
restore-keys: |
all-build-${{ hashFiles('**/composer.lock') }}
all-build-
- name: PHPStan
uses: phpDocumentor/phpstan-ga@0.12.3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: analyse src --level max --configuration phpstan.neon
psalm:
runs-on: ubuntu-latest
needs: [setup, phpunit]
steps:
- uses: actions/checkout@master
- name: Restore/cache vendor folder
uses: actions/cache@v1
with:
path: vendor
key: all-build-${{ hashFiles('**/composer.lock') }}
restore-keys: |
all-build-${{ hashFiles('**/composer.lock') }}
all-build-
- name: Restore/cache tools folder
uses: actions/cache@v1
with:
path: tools
key: all-tools-${{ github.sha }}
restore-keys: |
all-tools-${{ github.sha }}-
all-tools-
- name: Psalm
uses: docker://mickaelandrieu/psalm-ga
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
src/DocBlock/Description.php 0000666 00000006476 13617754202 0012037 0 ustar 00 create('This is a {@see Description}', $context);
*
* The description factory will interpret the given body and create a body template and list of tags from them, and pass
* that onto the constructor if this class.
*
* > The $context variable is a class of type {@see \phpDocumentor\Reflection\Types\Context} and contains the namespace
* > and the namespace aliases that apply to this DocBlock. These are used by the Factory to resolve and expand partial
* > type names and FQSENs.
*
* If you do not want to use the DescriptionFactory you can pass a body template and tag listing like this:
*
* $description = new Description(
* 'This is a %1$s',
* [ new See(new Fqsen('\phpDocumentor\Reflection\DocBlock\Description')) ]
* );
*
* It is generally recommended to use the Factory as that will also apply escaping rules, while the Description object
* is mainly responsible for rendering.
*
* @see DescriptionFactory to create a new Description.
* @see Description\Formatter for the formatting of the body and tags.
*/
class Description
{
/** @var string */
private $bodyTemplate;
/** @var Tag[] */
private $tags;
/**
* Initializes a Description with its body (template) and a listing of the tags used in the body template.
*
* @param Tag[] $tags
*/
public function __construct(string $bodyTemplate, array $tags = [])
{
$this->bodyTemplate = $bodyTemplate;
$this->tags = $tags;
}
/**
* Returns the tags for this DocBlock.
*
* @return Tag[]
*/
public function getTags() : array
{
return $this->tags;
}
/**
* Renders this description as a string where the provided formatter will format the tags in the expected string
* format.
*/
public function render(?Formatter $formatter = null) : string
{
if ($formatter === null) {
$formatter = new PassthroughFormatter();
}
$tags = [];
foreach ($this->tags as $tag) {
$tags[] = '{' . $formatter->format($tag) . '}';
}
return vsprintf($this->bodyTemplate, $tags);
}
/**
* Returns a plain string representation of this description.
*/
public function __toString() : string
{
return $this->render();
}
}
src/DocBlock/Serializer.php 0000666 00000011211 13617754202 0011644 0 ustar 00 indent = $indent;
$this->indentString = $indentString;
$this->isFirstLineIndented = $indentFirstLine;
$this->lineLength = $lineLength;
$this->tagFormatter = $tagFormatter ?: new PassthroughFormatter();
}
/**
* Generate a DocBlock comment.
*
* @param DocBlock $docblock The DocBlock to serialize.
*
* @return string The serialized doc block.
*/
public function getDocComment(DocBlock $docblock) : string
{
$indent = str_repeat($this->indentString, $this->indent);
$firstIndent = $this->isFirstLineIndented ? $indent : '';
// 3 === strlen(' * ')
$wrapLength = $this->lineLength ? $this->lineLength - strlen($indent) - 3 : null;
$text = $this->removeTrailingSpaces(
$indent,
$this->addAsterisksForEachLine(
$indent,
$this->getSummaryAndDescriptionTextBlock($docblock, $wrapLength)
)
);
$comment = $firstIndent . "/**\n";
if ($text) {
$comment .= $indent . ' * ' . $text . "\n";
$comment .= $indent . " *\n";
}
$comment = $this->addTagBlock($docblock, $wrapLength, $indent, $comment);
return $comment . $indent . ' */';
}
/**
* @return mixed
*/
private function removeTrailingSpaces(string $indent, string $text)
{
return str_replace(
sprintf("\n%s * \n", $indent),
sprintf("\n%s *\n", $indent),
$text
);
}
/**
* @return mixed
*/
private function addAsterisksForEachLine(string $indent, string $text)
{
return str_replace(
"\n",
sprintf("\n%s * ", $indent),
$text
);
}
private function getSummaryAndDescriptionTextBlock(DocBlock $docblock, ?int $wrapLength) : string
{
$text = $docblock->getSummary() . ((string) $docblock->getDescription() ? "\n\n" . $docblock->getDescription()
: '');
if ($wrapLength !== null) {
$text = wordwrap($text, $wrapLength);
return $text;
}
return $text;
}
private function addTagBlock(DocBlock $docblock, ?int $wrapLength, string $indent, string $comment) : string
{
foreach ($docblock->getTags() as $tag) {
$tagText = $this->tagFormatter->format($tag);
if ($wrapLength !== null) {
$tagText = wordwrap($tagText, $wrapLength);
}
$tagText = str_replace(
"\n",
sprintf("\n%s * ", $indent),
$tagText
);
$comment .= sprintf("%s * %s\n", $indent, $tagText);
}
return $comment;
}
}
src/DocBlock/DescriptionFactory.php 0000666 00000015136 13617754202 0013360 0 ustar 00 tagFactory = $tagFactory;
}
/**
* Returns the parsed text of this description.
*/
public function create(string $contents, ?TypeContext $context = null) : Description
{
$tokens = $this->lex($contents);
$count = count($tokens);
$tagCount = 0;
$tags = [];
for ($i = 1; $i < $count; $i += 2) {
$tags[] = $this->tagFactory->create($tokens[$i], $context);
$tokens[$i] = '%' . ++$tagCount . '$s';
}
//In order to allow "literal" inline tags, the otherwise invalid
//sequence "{@}" is changed to "@", and "{}" is changed to "}".
//"%" is escaped to "%%" because of vsprintf.
//See unit tests for examples.
for ($i = 0; $i < $count; $i += 2) {
$tokens[$i] = str_replace(['{@}', '{}', '%'], ['@', '}', '%%'], $tokens[$i]);
}
return new Description(implode('', $tokens), $tags);
}
/**
* Strips the contents from superfluous whitespace and splits the description into a series of tokens.
*
* @return string[] A series of tokens of which the description text is composed.
*/
private function lex(string $contents) : array
{
$contents = $this->removeSuperfluousStartingWhitespace($contents);
// performance optimalization; if there is no inline tag, don't bother splitting it up.
if (strpos($contents, '{@') === false) {
return [$contents];
}
$parts = preg_split(
'/\{
# "{@}" is not a valid inline tag. This ensures that we do not treat it as one, but treat it literally.
(?!@\})
# We want to capture the whole tag line, but without the inline tag delimiters.
(\@
# Match everything up to the next delimiter.
[^{}]*
# Nested inline tag content should not be captured, or it will appear in the result separately.
(?:
# Match nested inline tags.
(?:
# Because we did not catch the tag delimiters earlier, we must be explicit with them here.
# Notice that this also matches "{}", as a way to later introduce it as an escape sequence.
\{(?1)?\}
|
# Make sure we match hanging "{".
\{
)
# Match content after the nested inline tag.
[^{}]*
)* # If there are more inline tags, match them as well. We use "*" since there may not be any
# nested inline tags.
)
\}/Sux',
$contents,
0,
PREG_SPLIT_DELIM_CAPTURE
);
Assert::isArray($parts);
return $parts;
}
/**
* Removes the superfluous from a multi-line description.
*
* When a description has more than one line then it can happen that the second and subsequent lines have an
* additional indentation. This is commonly in use with tags like this:
*
* {@}since 1.1.0 This is an example
* description where we have an
* indentation in the second and
* subsequent lines.
*
* If we do not normalize the indentation then we have superfluous whitespace on the second and subsequent
* lines and this may cause rendering issues when, for example, using a Markdown converter.
*/
private function removeSuperfluousStartingWhitespace(string $contents) : string
{
$lines = explode("\n", $contents);
// if there is only one line then we don't have lines with superfluous whitespace and
// can use the contents as-is
if (count($lines) <= 1) {
return $contents;
}
// determine how many whitespace characters need to be stripped
$startingSpaceCount = 9999999;
for ($i = 1; $i < count($lines); ++$i) {
// lines with a no length do not count as they are not indented at all
if (strlen(trim($lines[$i])) === 0) {
continue;
}
// determine the number of prefixing spaces by checking the difference in line length before and after
// an ltrim
$startingSpaceCount = min($startingSpaceCount, strlen($lines[$i]) - strlen(ltrim($lines[$i])));
}
// strip the number of spaces from each line
if ($startingSpaceCount > 0) {
for ($i = 1; $i < count($lines); ++$i) {
$lines[$i] = substr($lines[$i], $startingSpaceCount);
}
}
return implode("\n", $lines);
}
}
src/DocBlock/StandardTagFactory.php 0000666 00000030061 13617754202 0013263 0 ustar 00 Important: each parameter in addition to the body variable for the `create` method must default to null, otherwise
* > it violates the constraint with the interface; it is recommended to use the {@see Assert::notNull()} method to
* > verify that a dependency is actually passed.
*
* This Factory also features a Service Locator component that is used to pass the right dependencies to the
* `create` method of a tag; each dependency should be registered as a service or as a parameter.
*
* When you want to use a Tag of your own with custom handling you need to call the `registerTagHandler` method, pass
* the name of the tag and a Fully Qualified Class Name pointing to a class that implements the Tag interface.
*/
final class StandardTagFactory implements TagFactory
{
/** PCRE regular expression matching a tag name. */
public const REGEX_TAGNAME = '[\w\-\_\\\\:]+';
/**
* @var string[] An array with a tag as a key, and an
* FQCN to a class that handles it as an array value.
*/
private $tagHandlerMappings = [
'author' => Author::class,
'covers' => Covers::class,
'deprecated' => Deprecated::class,
// 'example' => '\phpDocumentor\Reflection\DocBlock\Tags\Example',
'link' => LinkTag::class,
'method' => Method::class,
'param' => Param::class,
'property-read' => PropertyRead::class,
'property' => Property::class,
'property-write' => PropertyWrite::class,
'return' => Return_::class,
'see' => SeeTag::class,
'since' => Since::class,
'source' => Source::class,
'throw' => Throws::class,
'throws' => Throws::class,
'uses' => Uses::class,
'var' => Var_::class,
'version' => Version::class,
];
/**
* @var string[] An array with a anotation s a key, and an
* FQCN to a class that handles it as an array value.
*/
private $annotationMappings = [];
/**
* @var ReflectionParameter[][] a lazy-loading cache containing parameters
* for each tagHandler that has been used.
*/
private $tagHandlerParameterCache = [];
/** @var FqsenResolver */
private $fqsenResolver;
/**
* @var mixed[] an array representing a simple Service Locator where we can store parameters and
* services that can be inserted into the Factory Methods of Tag Handlers.
*/
private $serviceLocator = [];
/**
* Initialize this tag factory with the means to resolve an FQSEN and optionally a list of tag handlers.
*
* If no tag handlers are provided than the default list in the {@see self::$tagHandlerMappings} property
* is used.
*
* @see self::registerTagHandler() to add a new tag handler to the existing default list.
*
* @param string[] $tagHandlers
*/
public function __construct(FqsenResolver $fqsenResolver, ?array $tagHandlers = null)
{
$this->fqsenResolver = $fqsenResolver;
if ($tagHandlers !== null) {
$this->tagHandlerMappings = $tagHandlers;
}
$this->addService($fqsenResolver, FqsenResolver::class);
}
/**
* {@inheritDoc}
*/
public function create(string $tagLine, ?TypeContext $context = null) : Tag
{
if (!$context) {
$context = new TypeContext('');
}
[$tagName, $tagBody] = $this->extractTagParts($tagLine);
return $this->createTag(trim($tagBody), $tagName, $context);
}
/**
* {@inheritDoc}
*/
public function addParameter(string $name, $value) : void
{
$this->serviceLocator[$name] = $value;
}
/**
* {@inheritDoc}
*/
public function addService(object $service, ?string $alias = null) : void
{
$this->serviceLocator[$alias ?: get_class($service)] = $service;
}
/**
* {@inheritDoc}
*/
public function registerTagHandler(string $tagName, string $handler) : void
{
Assert::stringNotEmpty($tagName);
Assert::classExists($handler);
/** @var object $handler stupid hack to make phpstan happy. */
Assert::implementsInterface($handler, StaticMethod::class);
if (strpos($tagName, '\\') && $tagName[0] !== '\\') {
throw new InvalidArgumentException(
'A namespaced tag must have a leading backslash as it must be fully qualified'
);
}
$this->tagHandlerMappings[$tagName] = $handler;
}
/**
* Extracts all components for a tag.
*
* @return string[]
*/
private function extractTagParts(string $tagLine) : array
{
$matches = [];
if (!preg_match('/^@(' . self::REGEX_TAGNAME . ')((?:[\s\(\{])\s*([^\s].*)|$)/us', $tagLine, $matches)) {
throw new InvalidArgumentException(
'The tag "' . $tagLine . '" does not seem to be wellformed, please check it for errors'
);
}
if (count($matches) < 3) {
$matches[] = '';
}
return array_slice($matches, 1);
}
/**
* Creates a new tag object with the given name and body or returns null if the tag name was recognized but the
* body was invalid.
*/
private function createTag(string $body, string $name, TypeContext $context) : Tag
{
$handlerClassName = $this->findHandlerClassName($name, $context);
$arguments = $this->getArgumentsForParametersFromWiring(
$this->fetchParametersForHandlerFactoryMethod($handlerClassName),
$this->getServiceLocatorWithDynamicParameters($context, $name, $body)
);
try {
/** @var callable $callable */
$callable = [$handlerClassName, 'create'];
$tag = call_user_func_array($callable, $arguments);
return $tag ?? InvalidTag::create($body, $name);
} catch (InvalidArgumentException $e) {
return InvalidTag::create($body, $name)->withError($e);
}
}
/**
* Determines the Fully Qualified Class Name of the Factory or Tag (containing a Factory Method `create`).
*/
private function findHandlerClassName(string $tagName, TypeContext $context) : string
{
$handlerClassName = Generic::class;
if (isset($this->tagHandlerMappings[$tagName])) {
$handlerClassName = $this->tagHandlerMappings[$tagName];
} elseif ($this->isAnnotation($tagName)) {
// TODO: Annotation support is planned for a later stage and as such is disabled for now
$tagName = (string) $this->fqsenResolver->resolve($tagName, $context);
if (isset($this->annotationMappings[$tagName])) {
$handlerClassName = $this->annotationMappings[$tagName];
}
}
return $handlerClassName;
}
/**
* Retrieves the arguments that need to be passed to the Factory Method with the given Parameters.
*
* @param ReflectionParameter[] $parameters
* @param mixed[] $locator
*
* @return mixed[] A series of values that can be passed to the Factory Method of the tag whose parameters
* is provided with this method.
*/
private function getArgumentsForParametersFromWiring(array $parameters, array $locator) : array
{
$arguments = [];
foreach ($parameters as $parameter) {
$class = $parameter->getClass();
$typeHint = null;
if ($class !== null) {
$typeHint = $class->getName();
}
if (isset($locator[$typeHint])) {
$arguments[] = $locator[$typeHint];
continue;
}
$parameterName = $parameter->getName();
if (isset($locator[$parameterName])) {
$arguments[] = $locator[$parameterName];
continue;
}
$arguments[] = null;
}
return $arguments;
}
/**
* Retrieves a series of ReflectionParameter objects for the static 'create' method of the given
* tag handler class name.
*
* @return ReflectionParameter[]
*/
private function fetchParametersForHandlerFactoryMethod(string $handlerClassName) : array
{
if (!isset($this->tagHandlerParameterCache[$handlerClassName])) {
$methodReflection = new ReflectionMethod($handlerClassName, 'create');
$this->tagHandlerParameterCache[$handlerClassName] = $methodReflection->getParameters();
}
return $this->tagHandlerParameterCache[$handlerClassName];
}
/**
* Returns a copy of this class' Service Locator with added dynamic parameters,
* such as the tag's name, body and Context.
*
* @param TypeContext $context The Context (namespace and aliasses) that may be
* passed and is used to resolve FQSENs.
* @param string $tagName The name of the tag that may be
* passed onto the factory method of the Tag class.
* @param string $tagBody The body of the tag that may be
* passed onto the factory method of the Tag class.
*
* @return mixed[]
*/
private function getServiceLocatorWithDynamicParameters(
TypeContext $context,
string $tagName,
string $tagBody
) : array {
return array_merge(
$this->serviceLocator,
[
'name' => $tagName,
'body' => $tagBody,
TypeContext::class => $context,
]
);
}
/**
* Returns whether the given tag belongs to an annotation.
*
* @todo this method should be populated once we implement Annotation notation support.
*/
private function isAnnotation(string $tagContent) : bool
{
// 1. Contains a namespace separator
// 2. Contains parenthesis
// 3. Is present in a list of known annotations (make the algorithm smart by first checking is the last part
// of the annotation class name matches the found tag name
return false;
}
}
src/DocBlock/ExampleFinder.php 0000666 00000011145 13617754202 0012264 0 ustar 00 getFilePath();
$file = $this->getExampleFileContents($filename);
if (!$file) {
return sprintf('** File not found : %s **', $filename);
}
return implode('', array_slice($file, $example->getStartingLine() - 1, $example->getLineCount()));
}
/**
* Registers the project's root directory where an 'examples' folder can be expected.
*/
public function setSourceDirectory(string $directory = '') : void
{
$this->sourceDirectory = $directory;
}
/**
* Returns the project's root directory where an 'examples' folder can be expected.
*/
public function getSourceDirectory() : string
{
return $this->sourceDirectory;
}
/**
* Registers a series of directories that may contain examples.
*
* @param string[] $directories
*/
public function setExampleDirectories(array $directories) : void
{
$this->exampleDirectories = $directories;
}
/**
* Returns a series of directories that may contain examples.
*
* @return string[]
*/
public function getExampleDirectories() : array
{
return $this->exampleDirectories;
}
/**
* Attempts to find the requested example file and returns its contents or null if no file was found.
*
* This method will try several methods in search of the given example file, the first one it encounters is
* returned:
*
* 1. Iterates through all examples folders for the given filename
* 2. Checks the source folder for the given filename
* 3. Checks the 'examples' folder in the current working directory for examples
* 4. Checks the path relative to the current working directory for the given filename
*
* @return string[] all lines of the example file
*/
private function getExampleFileContents(string $filename) : ?array
{
$normalizedPath = null;
foreach ($this->exampleDirectories as $directory) {
$exampleFileFromConfig = $this->constructExamplePath($directory, $filename);
if (is_readable($exampleFileFromConfig)) {
$normalizedPath = $exampleFileFromConfig;
break;
}
}
if (!$normalizedPath) {
if (is_readable($this->getExamplePathFromSource($filename))) {
$normalizedPath = $this->getExamplePathFromSource($filename);
} elseif (is_readable($this->getExamplePathFromExampleDirectory($filename))) {
$normalizedPath = $this->getExamplePathFromExampleDirectory($filename);
} elseif (is_readable($filename)) {
$normalizedPath = $filename;
}
}
$lines = $normalizedPath && is_readable($normalizedPath) ? file($normalizedPath) : false;
return $lines !== false ? $lines : null;
}
/**
* Get example filepath based on the example directory inside your project.
*/
private function getExamplePathFromExampleDirectory(string $file) : string
{
return getcwd() . DIRECTORY_SEPARATOR . 'examples' . DIRECTORY_SEPARATOR . $file;
}
/**
* Returns a path to the example file in the given directory..
*/
private function constructExamplePath(string $directory, string $file) : string
{
return rtrim($directory, '\\/') . DIRECTORY_SEPARATOR . $file;
}
/**
* Get example filepath based on sourcecode.
*/
private function getExamplePathFromSource(string $file) : string
{
return sprintf(
'%s%s%s',
trim($this->getSourceDirectory(), '\\/'),
DIRECTORY_SEPARATOR,
trim($file, '"')
);
}
}
src/DocBlock/TagFactory.php 0000666 00000007112 13617754202 0011603 0 ustar 00 validateTagName($name);
$this->name = $name;
$this->description = $description;
}
/**
* Creates a new tag that represents any unknown tag type.
*
* @return static
*/
public static function create(
string $body,
string $name = '',
?DescriptionFactory $descriptionFactory = null,
?TypeContext $context = null
) : self {
Assert::stringNotEmpty($name);
Assert::notNull($descriptionFactory);
$description = $body !== '' ? $descriptionFactory->create($body, $context) : null;
return new static($name, $description);
}
/**
* Returns the tag as a serialized string
*/
public function __toString() : string
{
return $this->description ? $this->description->render() : '';
}
/**
* Validates if the tag name matches the expected format, otherwise throws an exception.
*/
private function validateTagName(string $name) : void
{
if (!preg_match('/^' . StandardTagFactory::REGEX_TAGNAME . '$/u', $name)) {
throw new InvalidArgumentException(
'The tag name "' . $name . '" is not wellformed. Tags may only consist of letters, underscores, '
. 'hyphens and backslashes.'
);
}
}
}
src/DocBlock/Tags/Formatter.php 0000666 00000001020 13617754202 0012371 0 ustar 00 refers = $refers;
$this->description = $description;
}
/**
* {@inheritdoc}
*/
public static function create(
string $body,
?FqsenResolver $typeResolver = null,
?DescriptionFactory $descriptionFactory = null,
?TypeContext $context = null
) : self {
Assert::notNull($typeResolver);
Assert::notNull($descriptionFactory);
$parts = preg_split('/\s+/Su', $body, 2);
Assert::isArray($parts);
$description = isset($parts[1]) ? $descriptionFactory->create($parts[1], $context) : null;
// https://tools.ietf.org/html/rfc2396#section-3
if (preg_match('/\w:\/\/\w/i', $parts[0])) {
return new static(new Url($parts[0]), $description);
}
return new static(new FqsenRef($typeResolver->resolve($parts[0], $context)), $description);
}
/**
* Returns the ref of this tag.
*/
public function getReference() : Reference
{
return $this->refers;
}
/**
* Returns a string representation of this tag.
*/
public function __toString() : string
{
return $this->refers . ($this->description ? ' ' . $this->description->render() : '');
}
}
src/DocBlock/Tags/Deprecated.php 0000666 00000005200 13617754202 0012472 0 ustar 00 version = $version;
$this->description = $description;
}
/**
* @return static
*/
public static function create(
?string $body,
?DescriptionFactory $descriptionFactory = null,
?TypeContext $context = null
) : self {
if (empty($body)) {
return new static();
}
$matches = [];
if (!preg_match('/^(' . self::REGEX_VECTOR . ')\s*(.+)?$/sux', $body, $matches)) {
return new static(
null,
$descriptionFactory !== null ? $descriptionFactory->create($body, $context) : null
);
}
Assert::notNull($descriptionFactory);
return new static(
$matches[1],
$descriptionFactory->create($matches[2] ?? '', $context)
);
}
/**
* Gets the version section of the tag.
*/
public function getVersion() : ?string
{
return $this->version;
}
/**
* Returns a string representation for this tag.
*/
public function __toString() : string
{
return ($this->version ?? '') . ($this->description ? ' ' . $this->description->render() : '');
}
}
src/DocBlock/Tags/InvalidTag.php 0000666 00000007414 13617754202 0012465 0 ustar 00 name = $name;
$this->body = $body;
}
public function getException() : ?Throwable
{
return $this->throwable;
}
public function getName() : string
{
return $this->name;
}
/**
* @return self
*
* @inheritDoc
*/
public static function create(string $body, string $name = '')
{
return new self($name, $body);
}
public function withError(Throwable $exception) : self
{
$this->flattenExceptionBacktrace($exception);
$tag = new self($this->name, $this->body);
$tag->throwable = $exception;
return $tag;
}
/**
* Removes all complex types from backtrace
*
* Not all objects are serializable. So we need to remove them from the
* stored exception to be sure that we do not break existing library usage.
*/
private function flattenExceptionBacktrace(Throwable $exception) : void
{
$traceProperty = (new ReflectionClass('Exception'))->getProperty('trace');
$traceProperty->setAccessible(true);
$flatten =
/** @param mixed $value */
static function (&$value) : void {
if ($value instanceof Closure) {
$closureReflection = new ReflectionFunction($value);
$value = sprintf(
'(Closure at %s:%s)',
$closureReflection->getFileName(),
$closureReflection->getStartLine()
);
} elseif (is_object($value)) {
$value = sprintf('object(%s)', get_class($value));
} elseif (is_resource($value)) {
$value = sprintf('resource(%s)', get_resource_type($value));
}
};
do {
$trace = array_map(
static function (array $call) use ($flatten) : array {
array_walk_recursive($call['args'], $flatten);
return $call;
},
$exception->getTrace()
);
$traceProperty->setValue($exception, $trace);
$exception = $exception->getPrevious();
} while ($exception !== null);
$traceProperty->setAccessible(false);
}
public function render(?Formatter $formatter = null) : string
{
if ($formatter === null) {
$formatter = new Formatter\PassthroughFormatter();
}
return $formatter->format($this);
}
public function __toString() : string
{
return $this->body;
}
}
src/DocBlock/Tags/PropertyWrite.php 0000666 00000006430 13617754202 0013277 0 ustar 00 name = 'property-write';
$this->variableName = $variableName;
$this->type = $type;
$this->description = $description;
}
/**
* {@inheritdoc}
*/
public static function create(
string $body,
?TypeResolver $typeResolver = null,
?DescriptionFactory $descriptionFactory = null,
?TypeContext $context = null
) : self {
Assert::stringNotEmpty($body);
Assert::notNull($typeResolver);
Assert::notNull($descriptionFactory);
[$firstPart, $body] = self::extractTypeFromBody($body);
$type = null;
$parts = preg_split('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE);
$variableName = '';
// if the first item that is encountered is not a variable; it is a type
if ($firstPart && (strlen($firstPart) > 0) && ($firstPart[0] !== '$')) {
$type = $typeResolver->resolve($firstPart, $context);
} else {
// first part is not a type; we should prepend it to the parts array for further processing
array_unshift($parts, $firstPart);
}
// if the next item starts with a $ or ...$ it must be the variable name
if (isset($parts[0]) && (strlen($parts[0]) > 0) && ($parts[0][0] === '$')) {
$variableName = array_shift($parts);
array_shift($parts);
if ($variableName !== null && strpos($variableName, '$') === 0) {
$variableName = substr($variableName, 1);
}
}
$description = $descriptionFactory->create(implode('', $parts), $context);
return new static($variableName, $type, $description);
}
/**
* Returns the variable's name.
*/
public function getVariableName() : ?string
{
return $this->variableName;
}
/**
* Returns a string representation for this tag.
*/
public function __toString() : string
{
return ($this->type ? $this->type . ' ' : '')
. ($this->variableName ? '$' . $this->variableName : '')
. ($this->description ? ' ' . $this->description : '');
}
}
src/DocBlock/Tags/Since.php 0000666 00000004676 13617754202 0011513 0 ustar 00 version = $version;
$this->description = $description;
}
public static function create(
?string $body,
?DescriptionFactory $descriptionFactory = null,
?TypeContext $context = null
) : ?self {
if (empty($body)) {
return new static();
}
$matches = [];
if (!preg_match('/^(' . self::REGEX_VECTOR . ')\s*(.+)?$/sux', $body, $matches)) {
return null;
}
Assert::notNull($descriptionFactory);
return new static(
$matches[1],
$descriptionFactory->create($matches[2] ?? '', $context)
);
}
/**
* Gets the version section of the tag.
*/
public function getVersion() : ?string
{
return $this->version;
}
/**
* Returns a string representation for this tag.
*/
public function __toString() : string
{
return (string) $this->version . ($this->description ? ' ' . (string) $this->description : '');
}
}
src/DocBlock/Tags/Method.php 0000666 00000016646 13617754202 0011672 0 ustar 00 |string> $arguments
*/
public function __construct(
string $methodName,
array $arguments = [],
?Type $returnType = null,
bool $static = false,
?Description $description = null
) {
Assert::stringNotEmpty($methodName);
if ($returnType === null) {
$returnType = new Void_();
}
$this->methodName = $methodName;
$this->arguments = $this->filterArguments($arguments);
$this->returnType = $returnType;
$this->isStatic = $static;
$this->description = $description;
}
/**
* {@inheritdoc}
*/
public static function create(
string $body,
?TypeResolver $typeResolver = null,
?DescriptionFactory $descriptionFactory = null,
?TypeContext $context = null
) : ?self {
Assert::stringNotEmpty($body);
Assert::notNull($typeResolver);
Assert::notNull($descriptionFactory);
// 1. none or more whitespace
// 2. optionally the keyword "static" followed by whitespace
// 3. optionally a word with underscores followed by whitespace : as
// type for the return value
// 4. then optionally a word with underscores followed by () and
// whitespace : as method name as used by phpDocumentor
// 5. then a word with underscores, followed by ( and any character
// until a ) and whitespace : as method name with signature
// 6. any remaining text : as description
if (!preg_match(
'/^
# Static keyword
# Declares a static method ONLY if type is also present
(?:
(static)
\s+
)?
# Return type
(?:
(
(?:[\w\|_\\\\]*\$this[\w\|_\\\\]*)
|
(?:
(?:[\w\|_\\\\]+)
# array notation
(?:\[\])*
)*
)
\s+
)?
# Method name
([\w_]+)
# Arguments
(?:
\(([^\)]*)\)
)?
\s*
# Description
(.*)
$/sux',
$body,
$matches
)) {
return null;
}
[, $static, $returnType, $methodName, $arguments, $description] = $matches;
$static = $static === 'static';
if ($returnType === '') {
$returnType = 'void';
}
$returnType = $typeResolver->resolve($returnType, $context);
$description = $descriptionFactory->create($description, $context);
if ($arguments !== '') {
$arguments = explode(',', $arguments);
foreach ($arguments as &$argument) {
$argument = explode(' ', self::stripRestArg(trim($argument)), 2);
if ($argument[0][0] === '$') {
$argumentName = substr($argument[0], 1);
$argumentType = new Mixed_();
} else {
$argumentType = $typeResolver->resolve($argument[0], $context);
$argumentName = '';
if (isset($argument[1])) {
$argument[1] = self::stripRestArg($argument[1]);
$argumentName = substr($argument[1], 1);
}
}
$argument = ['name' => $argumentName, 'type' => $argumentType];
}
} else {
$arguments = [];
}
return new static($methodName, $arguments, $returnType, $static, $description);
}
/**
* Retrieves the method name.
*/
public function getMethodName() : string
{
return $this->methodName;
}
/**
* @return string[][]
*/
public function getArguments() : array
{
return $this->arguments;
}
/**
* Checks whether the method tag describes a static method or not.
*
* @return bool TRUE if the method declaration is for a static method, FALSE otherwise.
*/
public function isStatic() : bool
{
return $this->isStatic;
}
public function getReturnType() : Type
{
return $this->returnType;
}
public function __toString() : string
{
$arguments = [];
foreach ($this->arguments as $argument) {
$arguments[] = $argument['type'] . ' $' . $argument['name'];
}
return trim(($this->isStatic() ? 'static ' : '')
. (string) $this->returnType . ' '
. $this->methodName
. '(' . implode(', ', $arguments) . ')'
. ($this->description ? ' ' . $this->description->render() : ''));
}
/**
* @param mixed[][]|string[] $arguments
*
* @return mixed[][]
*
* @psalm-param array|string> $arguments
* @psalm-return array> $arguments
*/
private function filterArguments(array $arguments = []) : array
{
$result = [];
foreach ($arguments as $argument) {
if (is_string($argument)) {
$argument = ['name' => $argument];
}
if (!isset($argument['type'])) {
$argument['type'] = new Mixed_();
}
$keys = array_keys($argument);
sort($keys);
if ($keys !== ['name', 'type']) {
throw new InvalidArgumentException(
'Arguments can only have the "name" and "type" fields, found: ' . var_export($keys, true)
);
}
$result[] = $argument;
}
return $result;
}
private static function stripRestArg(string $argument) : string
{
if (strpos($argument, '...') === 0) {
$argument = trim(substr($argument, 3));
}
return $argument;
}
}
src/DocBlock/Tags/Throws.php 0000666 00000003153 13617754202 0011725 0 ustar 00 name = 'throws';
$this->type = $type;
$this->description = $description;
}
/**
* {@inheritdoc}
*/
public static function create(
string $body,
?TypeResolver $typeResolver = null,
?DescriptionFactory $descriptionFactory = null,
?TypeContext $context = null
) : self {
Assert::notNull($typeResolver);
Assert::notNull($descriptionFactory);
[$type, $description] = self::extractTypeFromBody($body);
$type = $typeResolver->resolve($type, $context);
$description = $descriptionFactory->create($description, $context);
return new static($type, $description);
}
public function __toString() : string
{
return (string) $this->type . ' ' . (string) $this->description;
}
}
src/DocBlock/Tags/Covers.php 0000666 00000003760 13617754202 0011704 0 ustar 00 refers = $refers;
$this->description = $description;
}
/**
* {@inheritdoc}
*/
public static function create(
string $body,
?DescriptionFactory $descriptionFactory = null,
?FqsenResolver $resolver = null,
?TypeContext $context = null
) : self {
Assert::notEmpty($body);
Assert::notNull($descriptionFactory);
Assert::notNull($resolver);
$parts = preg_split('/\s+/Su', $body, 2);
Assert::isArray($parts);
return new static(
$resolver->resolve($parts[0], $context),
$descriptionFactory->create($parts[1] ?? '', $context)
);
}
/**
* Returns the structural element this tag refers to.
*/
public function getReference() : Fqsen
{
return $this->refers;
}
/**
* Returns a string representation of this tag.
*/
public function __toString() : string
{
return $this->refers . ($this->description ? ' ' . $this->description->render() : '');
}
}
src/DocBlock/Tags/Reference/Fqsen.php 0000666 00000001440 13617754202 0013406 0 ustar 00 fqsen = $fqsen;
}
/**
* @return string string representation of the referenced fqsen
*/
public function __toString() : string
{
return (string) $this->fqsen;
}
}
src/DocBlock/Tags/Reference/Url.php 0000666 00000001301 13617754202 0013070 0 ustar 00 uri = $uri;
}
public function __toString() : string
{
return $this->uri;
}
}
src/DocBlock/Tags/Reference/Reference.php 0000666 00000000715 13617754202 0014234 0 ustar 00 refers = $refers;
$this->description = $description;
}
/**
* {@inheritdoc}
*/
public static function create(
string $body,
?FqsenResolver $resolver = null,
?DescriptionFactory $descriptionFactory = null,
?TypeContext $context = null
) : self {
Assert::notNull($resolver);
Assert::notNull($descriptionFactory);
$parts = preg_split('/\s+/Su', $body, 2);
Assert::isArray($parts);
Assert::allString($parts);
return new static(
$resolver->resolve($parts[0], $context),
$descriptionFactory->create($parts[1] ?? '', $context)
);
}
/**
* Returns the structural element this tag refers to.
*/
public function getReference() : Fqsen
{
return $this->refers;
}
/**
* Returns a string representation of this tag.
*/
public function __toString() : string
{
return $this->refers . ' ' . (string) $this->description;
}
}
src/DocBlock/Tags/Property.php 0000666 00000006417 13617754202 0012271 0 ustar 00 name = 'property';
$this->variableName = $variableName;
$this->type = $type;
$this->description = $description;
}
/**
* {@inheritdoc}
*/
public static function create(
string $body,
?TypeResolver $typeResolver = null,
?DescriptionFactory $descriptionFactory = null,
?TypeContext $context = null
) : self {
Assert::stringNotEmpty($body);
Assert::notNull($typeResolver);
Assert::notNull($descriptionFactory);
[$firstPart, $body] = self::extractTypeFromBody($body);
$type = null;
$parts = preg_split('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE);
$variableName = '';
// if the first item that is encountered is not a variable; it is a type
if ($firstPart && (strlen($firstPart) > 0) && ($firstPart[0] !== '$')) {
$type = $typeResolver->resolve($firstPart, $context);
} else {
// first part is not a type; we should prepend it to the parts array for further processing
array_unshift($parts, $firstPart);
}
// if the next item starts with a $ or ...$ it must be the variable name
if (isset($parts[0]) && ($parts[0] !== '') && (strpos($parts[0], '$') === 0)) {
$variableName = array_shift($parts);
array_shift($parts);
if ($variableName !== null && strpos($variableName, '$') === 0) {
$variableName = substr($variableName, 1);
}
}
$description = $descriptionFactory->create(implode('', $parts), $context);
return new static($variableName, $type, $description);
}
/**
* Returns the variable's name.
*/
public function getVariableName() : ?string
{
return $this->variableName;
}
/**
* Returns a string representation for this tag.
*/
public function __toString() : string
{
return ($this->type ? $this->type . ' ' : '')
. ($this->variableName ? '$' . $this->variableName : '')
. ($this->description ? ' ' . $this->description : '');
}
}
src/DocBlock/Tags/Formatter/AlignFormatter.php 0000666 00000002243 13617754202 0015317 0 ustar 00 maxLen = max($this->maxLen, strlen($tag->getName()));
}
}
/**
* Formats the given tag to return a simple plain text version.
*/
public function format(Tag $tag) : string
{
return '@' . $tag->getName() .
str_repeat(
' ',
$this->maxLen - strlen($tag->getName()) + 1
) .
$tag;
}
}
src/DocBlock/Tags/Formatter/PassthroughFormatter.php 0000666 00000001243 13617754202 0016573 0 ustar 00 getName() . ' ' . $tag);
}
}
src/DocBlock/Tags/Source.php 0000666 00000006041 13617754202 0011676 0 ustar 00 startingLine = (int) $startingLine;
$this->lineCount = $lineCount !== null ? (int) $lineCount : null;
$this->description = $description;
}
/**
* {@inheritdoc}
*/
public static function create(
string $body,
?DescriptionFactory $descriptionFactory = null,
?TypeContext $context = null
) : self {
Assert::stringNotEmpty($body);
Assert::notNull($descriptionFactory);
$startingLine = 1;
$lineCount = null;
$description = null;
// Starting line / Number of lines / Description
if (preg_match('/^([1-9]\d*)\s*(?:((?1))\s+)?(.*)$/sux', $body, $matches)) {
$startingLine = (int) $matches[1];
if (isset($matches[2]) && $matches[2] !== '') {
$lineCount = (int) $matches[2];
}
$description = $matches[3];
}
return new static($startingLine, $lineCount, $descriptionFactory->create($description??'', $context));
}
/**
* Gets the starting line.
*
* @return int The starting line, relative to the structural element's
* location.
*/
public function getStartingLine() : int
{
return $this->startingLine;
}
/**
* Returns the number of lines.
*
* @return int|null The number of lines, relative to the starting line. NULL
* means "to the end".
*/
public function getLineCount() : ?int
{
return $this->lineCount;
}
public function __toString() : string
{
return $this->startingLine
. ($this->lineCount !== null ? ' ' . $this->lineCount : '')
. ($this->description ? ' ' . (string) $this->description : '');
}
}
src/DocBlock/Tags/BaseTag.php 0000666 00000002173 13617754202 0011746 0 ustar 00 name;
}
public function getDescription() : ?Description
{
return $this->description;
}
public function render(?Formatter $formatter = null) : string
{
if ($formatter === null) {
$formatter = new Formatter\PassthroughFormatter();
}
return $formatter->format($this);
}
}
src/DocBlock/Tags/Author.php 0000666 00000004436 13617754202 0011706 0 ustar 00 authorName = $authorName;
$this->authorEmail = $authorEmail;
}
/**
* Gets the author's name.
*
* @return string The author's name.
*/
public function getAuthorName() : string
{
return $this->authorName;
}
/**
* Returns the author's email.
*
* @return string The author's email.
*/
public function getEmail() : string
{
return $this->authorEmail;
}
/**
* Returns this tag in string form.
*/
public function __toString() : string
{
return $this->authorName . (strlen($this->authorEmail) ? ' <' . $this->authorEmail . '>' : '');
}
/**
* Attempts to create a new Author object based on †he tag body.
*/
public static function create(string $body) : ?self
{
$splitTagContent = preg_match('/^([^\<]*)(?:\<([^\>]*)\>)?$/u', $body, $matches);
if (!$splitTagContent) {
return null;
}
$authorName = trim($matches[1]);
$email = isset($matches[2]) ? trim($matches[2]) : '';
return new static($authorName, $email);
}
}
src/DocBlock/Tags/Link.php 0000666 00000003374 13617754202 0011341 0 ustar 00 link = $link;
$this->description = $description;
}
/**
* {@inheritdoc}
*/
public static function create(
string $body,
?DescriptionFactory $descriptionFactory = null,
?TypeContext $context = null
) : self {
Assert::notNull($descriptionFactory);
$parts = preg_split('/\s+/Su', $body, 2);
Assert::isArray($parts);
$description = isset($parts[1]) ? $descriptionFactory->create($parts[1], $context) : null;
return new static($parts[0], $description);
}
/**
* Gets the link
*/
public function getLink() : string
{
return $this->link;
}
/**
* Returns a string representation for this tag.
*/
public function __toString() : string
{
return $this->link . ($this->description ? ' ' . $this->description->render() : '');
}
}
src/DocBlock/Tags/Version.php 0000666 00000005064 13617754202 0012067 0 ustar 00 version = $version;
$this->description = $description;
}
public static function create(
?string $body,
?DescriptionFactory $descriptionFactory = null,
?TypeContext $context = null
) : ?self {
if (empty($body)) {
return new static();
}
$matches = [];
if (!preg_match('/^(' . self::REGEX_VECTOR . ')\s*(.+)?$/sux', $body, $matches)) {
return null;
}
$description = null;
if ($descriptionFactory !== null) {
$description = $descriptionFactory->create($matches[2] ?? '', $context);
}
return new static(
$matches[1],
$description
);
}
/**
* Gets the version section of the tag.
*/
public function getVersion() : ?string
{
return $this->version;
}
/**
* Returns a string representation for this tag.
*/
public function __toString() : string
{
return ((string) $this->version) .
($this->description instanceof Description ? ' ' . $this->description->render() : '');
}
}
src/DocBlock/Tags/PropertyRead.php 0000666 00000006435 13617754202 0013065 0 ustar 00 name = 'property-read';
$this->variableName = $variableName;
$this->type = $type;
$this->description = $description;
}
/**
* {@inheritdoc}
*/
public static function create(
string $body,
?TypeResolver $typeResolver = null,
?DescriptionFactory $descriptionFactory = null,
?TypeContext $context = null
) : self {
Assert::stringNotEmpty($body);
Assert::notNull($typeResolver);
Assert::notNull($descriptionFactory);
[$firstPart, $body] = self::extractTypeFromBody($body);
$type = null;
$parts = preg_split('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE);
$variableName = '';
// if the first item that is encountered is not a variable; it is a type
if ($firstPart && (strlen($firstPart) > 0) && ($firstPart[0] !== '$')) {
$type = $typeResolver->resolve($firstPart, $context);
} else {
// first part is not a type; we should prepend it to the parts array for further processing
array_unshift($parts, $firstPart);
}
// if the next item starts with a $ or ...$ it must be the variable name
if (isset($parts[0]) && ($parts[0] !== '') && (strpos($parts[0], '$') === 0)) {
$variableName = array_shift($parts);
array_shift($parts);
if ($variableName !== null && strpos($variableName, '$') === 0) {
$variableName = substr($variableName, 1);
}
}
$description = $descriptionFactory->create(implode('', $parts), $context);
return new static($variableName, $type, $description);
}
/**
* Returns the variable's name.
*/
public function getVariableName() : ?string
{
return $this->variableName;
}
/**
* Returns a string representation for this tag.
*/
public function __toString() : string
{
return ($this->type ? $this->type . ' ' : '')
. ($this->variableName ? '$' . $this->variableName : '')
. ($this->description ? ' ' . $this->description : '');
}
}
src/DocBlock/Tags/Factory/StaticMethod.php 0000666 00000000642 13617754202 0014436 0 ustar 00 name = 'param';
$this->variableName = $variableName;
$this->type = $type;
$this->isVariadic = $isVariadic;
$this->description = $description;
}
/**
* {@inheritdoc}
*/
public static function create(
string $body,
?TypeResolver $typeResolver = null,
?DescriptionFactory $descriptionFactory = null,
?TypeContext $context = null
) : self {
Assert::stringNotEmpty($body);
Assert::notNull($typeResolver);
Assert::notNull($descriptionFactory);
[$firstPart, $body] = self::extractTypeFromBody($body);
$type = null;
$parts = preg_split('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE);
$variableName = '';
$isVariadic = false;
// if the first item that is encountered is not a variable; it is a type
if ($firstPart && (strlen($firstPart) > 0) && ($firstPart[0] !== '$')) {
$type = $typeResolver->resolve($firstPart, $context);
} else {
// first part is not a type; we should prepend it to the parts array for further processing
array_unshift($parts, $firstPart);
}
// if the next item starts with a $ or ...$ it must be the variable name
if (isset($parts[0])
&& (strlen($parts[0]) > 0)
&& ($parts[0][0] === '$' || substr($parts[0], 0, 4) === '...$')
) {
$variableName = array_shift($parts);
array_shift($parts);
if ($variableName !== null && strpos($variableName, '...') === 0) {
$isVariadic = true;
$variableName = substr($variableName, 3);
}
if ($variableName !== null && strpos($variableName, '$') === 0) {
$variableName = substr($variableName, 1);
}
}
$description = $descriptionFactory->create(implode('', $parts), $context);
return new static($variableName, $type, $isVariadic, $description);
}
/**
* Returns the variable's name.
*/
public function getVariableName() : ?string
{
return $this->variableName;
}
/**
* Returns whether this tag is variadic.
*/
public function isVariadic() : bool
{
return $this->isVariadic;
}
/**
* Returns a string representation for this tag.
*/
public function __toString() : string
{
return ($this->type ? $this->type . ' ' : '')
. ($this->isVariadic() ? '...' : '')
. ($this->variableName !== null ? '$' . $this->variableName : '')
. ($this->description ? ' ' . $this->description : '');
}
}
src/DocBlock/Tags/TagWithType.php 0000666 00000002632 13617754202 0012651 0 ustar 00 type;
}
/**
* @return string[]
*/
protected static function extractTypeFromBody(string $body) : array
{
$type = '';
$nestingLevel = 0;
for ($i = 0; $i < strlen($body); $i++) {
$character = $body[$i];
if (trim($character) === '' && $nestingLevel === 0) {
break;
}
$type .= $character;
if (in_array($character, ['<', '(', '[', '{'])) {
$nestingLevel++;
continue;
}
if (in_array($character, ['>', ')', ']', '}'])) {
$nestingLevel--;
continue;
}
}
$description = trim(substr($body, strlen($type)));
return [$type, $description];
}
}
src/DocBlock/Tags/Return_.php 0000666 00000003160 13617754202 0012053 0 ustar 00 name = 'return';
$this->type = $type;
$this->description = $description;
}
/**
* {@inheritdoc}
*/
public static function create(
string $body,
?TypeResolver $typeResolver = null,
?DescriptionFactory $descriptionFactory = null,
?TypeContext $context = null
) : self {
Assert::notNull($typeResolver);
Assert::notNull($descriptionFactory);
[$type, $description] = self::extractTypeFromBody($body);
$type = $typeResolver->resolve($type, $context);
$description = $descriptionFactory->create($description, $context);
return new static($type, $description);
}
public function __toString() : string
{
return ($this->type ?: 'mixed') . ' ' . (string) $this->description;
}
}
src/DocBlock/Tags/Example.php 0000666 00000011152 13617754202 0012030 0 ustar 00 filePath = $filePath;
$this->startingLine = $startingLine;
$this->lineCount = $lineCount;
if ($content !== null) {
$this->content = trim((string) $content);
}
$this->isURI = $isURI;
}
public function getContent() : string
{
if ($this->content === null) {
$filePath = '"' . $this->filePath . '"';
if ($this->isURI) {
$filePath = $this->isUriRelative($this->filePath)
? str_replace('%2F', '/', rawurlencode($this->filePath))
: $this->filePath;
}
return trim($filePath);
}
return $this->content;
}
public function getDescription() : ?string
{
return $this->content;
}
/**
* {@inheritdoc}
*/
public static function create(string $body) : ?Tag
{
// File component: File path in quotes or File URI / Source information
if (!preg_match('/^(?:\"([^\"]+)\"|(\S+))(?:\s+(.*))?$/sux', $body, $matches)) {
return null;
}
$filePath = null;
$fileUri = null;
if ($matches[1] !== '') {
$filePath = $matches[1];
} else {
$fileUri = $matches[2];
}
$startingLine = 1;
$lineCount = 0;
$description = null;
if (array_key_exists(3, $matches)) {
$description = $matches[3];
// Starting line / Number of lines / Description
if (preg_match('/^([1-9]\d*)(?:\s+((?1))\s*)?(.*)$/sux', $matches[3], $contentMatches)) {
$startingLine = (int) $contentMatches[1];
if (isset($contentMatches[2]) && $contentMatches[2] !== '') {
$lineCount = (int) $contentMatches[2];
}
if (array_key_exists(3, $contentMatches)) {
$description = $contentMatches[3];
}
}
}
return new static(
$filePath ?? ($fileUri ?? ''),
$fileUri !== null,
$startingLine,
$lineCount,
$description
);
}
/**
* Returns the file path.
*
* @return string Path to a file to use as an example.
* May also be an absolute URI.
*/
public function getFilePath() : string
{
return $this->filePath;
}
/**
* Returns a string representation for this tag.
*/
public function __toString() : string
{
return $this->filePath . ($this->content ? ' ' . $this->content : '');
}
/**
* Returns true if the provided URI is relative or contains a complete scheme (and thus is absolute).
*/
private function isUriRelative(string $uri) : bool
{
return strpos($uri, ':') === false;
}
public function getStartingLine() : int
{
return $this->startingLine;
}
public function getLineCount() : int
{
return $this->lineCount;
}
public function getName() : string
{
return 'example';
}
public function render(?Formatter $formatter = null) : string
{
if ($formatter === null) {
$formatter = new Formatter\PassthroughFormatter();
}
return $formatter->format($this);
}
}
src/DocBlock/Tags/Var_.php 0000666 00000006371 13617754202 0011333 0 ustar 00 name = 'var';
$this->variableName = $variableName;
$this->type = $type;
$this->description = $description;
}
/**
* {@inheritdoc}
*/
public static function create(
string $body,
?TypeResolver $typeResolver = null,
?DescriptionFactory $descriptionFactory = null,
?TypeContext $context = null
) : self {
Assert::stringNotEmpty($body);
Assert::notNull($typeResolver);
Assert::notNull($descriptionFactory);
[$firstPart, $body] = self::extractTypeFromBody($body);
$parts = preg_split('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE);
$type = null;
$variableName = '';
// if the first item that is encountered is not a variable; it is a type
if ($firstPart && (strlen($firstPart) > 0) && ($firstPart[0] !== '$')) {
$type = $typeResolver->resolve($firstPart, $context);
} else {
// first part is not a type; we should prepend it to the parts array for further processing
array_unshift($parts, $firstPart);
}
// if the next item starts with a $ or ...$ it must be the variable name
if (isset($parts[0]) && ($parts[0] !== '') && (strpos($parts[0], '$') === 0)) {
$variableName = array_shift($parts);
array_shift($parts);
if ($variableName !== null && strpos($variableName, '$') === 0) {
$variableName = substr($variableName, 1);
}
}
$description = $descriptionFactory->create(implode('', $parts), $context);
return new static($variableName, $type, $description);
}
/**
* Returns the variable's name.
*/
public function getVariableName() : ?string
{
return $this->variableName;
}
/**
* Returns a string representation for this tag.
*/
public function __toString() : string
{
return ($this->type ? $this->type . ' ' : '')
. (empty($this->variableName) ? '' : ('$' . $this->variableName))
. ($this->description ? ' ' . $this->description : '');
}
}
src/DocBlockFactoryInterface.php 0000666 00000001063 13617754202 0012710 0 ustar 00 summary = $summary;
$this->description = $description ?: new DocBlock\Description('');
foreach ($tags as $tag) {
$this->addTag($tag);
}
$this->context = $context;
$this->location = $location;
$this->isTemplateEnd = $isTemplateEnd;
$this->isTemplateStart = $isTemplateStart;
}
public function getSummary() : string
{
return $this->summary;
}
public function getDescription() : DocBlock\Description
{
return $this->description;
}
/**
* Returns the current context.
*/
public function getContext() : ?Types\Context
{
return $this->context;
}
/**
* Returns the current location.
*/
public function getLocation() : ?Location
{
return $this->location;
}
/**
* Returns whether this DocBlock is the start of a Template section.
*
* A Docblock may serve as template for a series of subsequent DocBlocks. This is indicated by a special marker
* (`#@+`) that is appended directly after the opening `/**` of a DocBlock.
*
* An example of such an opening is:
*
* ```
* /**#@+
* * My DocBlock
* * /
* ```
*
* The description and tags (not the summary!) are copied onto all subsequent DocBlocks and also applied to all
* elements that follow until another DocBlock is found that contains the closing marker (`#@-`).
*
* @see self::isTemplateEnd() for the check whether a closing marker was provided.
*/
public function isTemplateStart() : bool
{
return $this->isTemplateStart;
}
/**
* Returns whether this DocBlock is the end of a Template section.
*
* @see self::isTemplateStart() for a more complete description of the Docblock Template functionality.
*/
public function isTemplateEnd() : bool
{
return $this->isTemplateEnd;
}
/**
* Returns the tags for this DocBlock.
*
* @return Tag[]
*/
public function getTags() : array
{
return $this->tags;
}
/**
* Returns an array of tags matching the given name. If no tags are found
* an empty array is returned.
*
* @param string $name String to search by.
*
* @return Tag[]
*/
public function getTagsByName(string $name) : array
{
$result = [];
/** @var Tag $tag */
foreach ($this->getTags() as $tag) {
if ($tag->getName() !== $name) {
continue;
}
$result[] = $tag;
}
return $result;
}
/**
* Checks if a tag of a certain type is present in this DocBlock.
*
* @param string $name Tag name to check for.
*/
public function hasTag(string $name) : bool
{
/** @var Tag $tag */
foreach ($this->getTags() as $tag) {
if ($tag->getName() === $name) {
return true;
}
}
return false;
}
/**
* Remove a tag from this DocBlock.
*
* @param Tag $tagToRemove The tag to remove.
*/
public function removeTag(Tag $tagToRemove) : void
{
foreach ($this->tags as $key => $tag) {
if ($tag === $tagToRemove) {
unset($this->tags[$key]);
break;
}
}
}
/**
* Adds a tag to this DocBlock.
*
* @param Tag $tag The tag to add.
*/
private function addTag(Tag $tag) : void
{
$this->tags[] = $tag;
}
}
src/DocBlockFactory.php 0000666 00000022335 13617754202 0011074 0 ustar 00 descriptionFactory = $descriptionFactory;
$this->tagFactory = $tagFactory;
}
/**
* Factory method for easy instantiation.
*
* @param string[] $additionalTags
*/
public static function createInstance(array $additionalTags = []) : self
{
$fqsenResolver = new FqsenResolver();
$tagFactory = new StandardTagFactory($fqsenResolver);
$descriptionFactory = new DescriptionFactory($tagFactory);
$tagFactory->addService($descriptionFactory);
$tagFactory->addService(new TypeResolver($fqsenResolver));
$docBlockFactory = new self($descriptionFactory, $tagFactory);
foreach ($additionalTags as $tagName => $tagHandler) {
$docBlockFactory->registerTagHandler($tagName, $tagHandler);
}
return $docBlockFactory;
}
/**
* @param object|string $docblock A string containing the DocBlock to parse or an object supporting the
* getDocComment method (such as a ReflectionClass object).
*/
public function create($docblock, ?Types\Context $context = null, ?Location $location = null) : DocBlock
{
if (is_object($docblock)) {
if (!method_exists($docblock, 'getDocComment')) {
$exceptionMessage = 'Invalid object passed; the given object must support the getDocComment method';
throw new InvalidArgumentException($exceptionMessage);
}
$docblock = $docblock->getDocComment();
}
Assert::stringNotEmpty($docblock);
if ($context === null) {
$context = new Types\Context('');
}
$parts = $this->splitDocBlock($this->stripDocComment($docblock));
[$templateMarker, $summary, $description, $tags] = $parts;
return new DocBlock(
$summary,
$description ? $this->descriptionFactory->create($description, $context) : null,
$this->parseTagBlock($tags, $context),
$context,
$location,
$templateMarker === '#@+',
$templateMarker === '#@-'
);
}
public function registerTagHandler(string $tagName, string $handler) : void
{
$this->tagFactory->registerTagHandler($tagName, $handler);
}
/**
* Strips the asterisks from the DocBlock comment.
*
* @param string $comment String containing the comment text.
*/
private function stripDocComment(string $comment) : string
{
/** @var string $comment */
$comment = preg_replace('#[ \t]*(?:\/\*\*|\*\/|\*)?[ \t]{0,1}(.*)?#u', '$1', $comment);
$comment = trim($comment);
// reg ex above is not able to remove */ from a single line docblock
if (substr($comment, -2) === '*/') {
$comment = trim(substr($comment, 0, -2));
}
return str_replace(["\r\n", "\r"], "\n", $comment);
}
// phpcs:disable SlevomatCodingStandard.Commenting.ForbiddenAnnotations.AnnotationForbidden
/**
* Splits the DocBlock into a template marker, summary, description and block of tags.
*
* @param string $comment Comment to split into the sub-parts.
*
* @return string[] containing the template marker (if any), summary, description and a string containing the tags.
*
* @author Mike van Riel for extending the regex with template marker support.
*
* @author Richard van Velzen (@_richardJ) Special thanks to Richard for the regex responsible for the split.
*/
private function splitDocBlock(string $comment) : array
{
// Performance improvement cheat: if the first character is an @ then only tags are in this DocBlock. This
// method does not split tags so we return this verbatim as the fourth result (tags). This saves us the
// performance impact of running a regular expression
if (strpos($comment, '@') === 0) {
return ['', '', '', $comment];
}
// clears all extra horizontal whitespace from the line endings to prevent parsing issues
/** @var string $comment */
$comment = preg_replace('/\h*$/Sum', '', $comment);
/*
* Splits the docblock into a template marker, summary, description and tags section.
*
* - The template marker is empty, #@+ or #@- if the DocBlock starts with either of those (a newline may
* occur after it and will be stripped).
* - The short description is started from the first character until a dot is encountered followed by a
* newline OR two consecutive newlines (horizontal whitespace is taken into account to consider spacing
* errors). This is optional.
* - The long description, any character until a new line is encountered followed by an @ and word
* characters (a tag). This is optional.
* - Tags; the remaining characters
*
* Big thanks to RichardJ for contributing this Regular Expression
*/
preg_match(
'/
\A
# 1. Extract the template marker
(?:(\#\@\+|\#\@\-)\n?)?
# 2. Extract the summary
(?:
(?! @\pL ) # The summary may not start with an @
(
[^\n.]+
(?:
(?! \. \n | \n{2} ) # End summary upon a dot followed by newline or two newlines
[\n.]* (?! [ \t]* @\pL ) # End summary when an @ is found as first character on a new line
[^\n.]+ # Include anything else
)*
\.?
)?
)
# 3. Extract the description
(?:
\s* # Some form of whitespace _must_ precede a description because a summary must be there
(?! @\pL ) # The description may not start with an @
(
[^\n]+
(?: \n+
(?! [ \t]* @\pL ) # End description when an @ is found as first character on a new line
[^\n]+ # Include anything else
)*
)
)?
# 4. Extract the tags (anything that follows)
(\s+ [\s\S]*)? # everything that follows
/ux',
$comment,
$matches
);
array_shift($matches);
while (count($matches) < 4) {
$matches[] = '';
}
return $matches;
}
/**
* Creates the tag objects.
*
* @param string $tags Tag block to parse.
* @param Types\Context $context Context of the parsed Tag
*
* @return DocBlock\Tag[]
*/
private function parseTagBlock(string $tags, Types\Context $context) : array
{
$tags = $this->filterTagBlock($tags);
if ($tags === null) {
return [];
}
$result = [];
$lines = $this->splitTagBlockIntoTagLines($tags);
foreach ($lines as $key => $tagLine) {
$result[$key] = $this->tagFactory->create(trim($tagLine), $context);
}
return $result;
}
/**
* @return string[]
*/
private function splitTagBlockIntoTagLines(string $tags) : array
{
$result = [];
foreach (explode("\n", $tags) as $tag_line) {
if (isset($tag_line[0]) && ($tag_line[0] === '@')) {
$result[] = $tag_line;
} else {
$result[count($result) - 1] .= "\n" . $tag_line;
}
}
return $result;
}
private function filterTagBlock(string $tags) : ?string
{
$tags = trim($tags);
if (!$tags) {
return null;
}
if ($tags[0] !== '@') {
// @codeCoverageIgnoreStart
// Can't simulate this; this only happens if there is an error with the parsing of the DocBlock that
// we didn't foresee.
throw new LogicException('A tag block started with text instead of an at-sign(@): ' . $tags);
// @codeCoverageIgnoreEnd
}
return $tags;
}
}
phive.xml 0000666 00000000435 13617754202 0006416 0 ustar 00
composer.json 0000666 00000002126 13617754202 0007302 0 ustar 00 {
"name": "phpdocumentor/reflection-docblock",
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Mike van Riel",
"email": "me@mikevanriel.com"
},
{
"name": "Jaap van Otterdijk",
"email": "account@ijaap.nl"
}
],
"require": {
"php": "^7.2",
"phpdocumentor/type-resolver": "^1.0",
"webmozart/assert": "^1",
"phpdocumentor/reflection-common": "^2.0",
"ext-filter": "^7.1"
},
"require-dev": {
"mockery/mockery": "^1",
"doctrine/instantiator": "^1"
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"phpDocumentor\\Reflection\\": "tests/unit"
}
},
"extra": {
"branch-alias": {
"dev-master": "5.x-dev"
}
}
}
Makefile 0000666 00000002252 13617754202 0006220 0 ustar 00 .PHONY: install-phive
install-phive:
mkdir tools; \
wget -O tools/phive.phar https://phar.io/releases/phive.phar; \
wget -O tools/phive.phar.asc https://phar.io/releases/phive.phar.asc; \
gpg --keyserver pool.sks-keyservers.net --recv-keys 0x9D8A98B29B2D5D79; \
gpg --verify tools/phive.phar.asc tools/phive.phar; \
chmod +x tools/phive.phar
.PHONY: setup
setup: install-phive
docker run -it --rm -v${PWD}:/opt/project -w /opt/project phpdoc/phar-ga:latest php tools/phive.phar install --force-accept-unsigned
.PHONY: phpcs
phpcs:
docker run -it --rm -v${PWD}:/opt/project -w /opt/project phpdoc/phpcs-ga:v1.0.0 -s
.PHONY: phpcbf
phpcbf:
docker run -it --rm -v${PWD}:/opt/project -w /opt/project phpdoc/phpcs-ga:v1.0.0 phpcbf
.PHONY: phpstan
phpstan:
docker run -it --rm -v${PWD}:/opt/project -w /opt/project phpdoc/phpstan-ga:latest analyse src --no-progress --level max --configuration phpstan.neon
.PHONY: psaml
psalm:
docker run -it --rm -v${PWD}:/opt/project -w /opt/project mickaelandrieu/psalm-ga
.PHONY: test
test:
docker run -it --rm -v${PWD}:/opt/project -w /opt/project php:7.4-pcov tools/phpunit
.PHONY: pre-commit-test
pre-commit-test: test phpcs phpstan psalm
.dependabot/config.yml 0000666 00000000472 13617754202 0010735 0 ustar 00 version: 1
update_configs:
- package_manager: "php:composer"
directory: "/"
update_schedule: "weekly"
automerged_updates:
- match:
dependency_type: "development"
update_type: "all"
- match:
dependency_type: "production"
update_type: "semver:patch"
phpstan.neon 0000666 00000000530 13617754202 0007113 0 ustar 00 includes:
- /composer/vendor/phpstan/phpstan-mockery/extension.neon
- /composer/vendor/phpstan/phpstan-webmozart-assert/extension.neon
parameters:
ignoreErrors:
# false positive
- '#Method phpDocumentor\Reflection\DocBlock\Tags\Method::filterArguments() should return array but returns array#'