File "Annotation.php"
Full Path: /var/www/html/back/vendor/friendsofphp/php-cs-fixer/src/DocBlock/Annotation.php
File size: 9.15 KB
MIME-type: text/x-php
Charset: utf-8
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\DocBlock;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
/**
* This represents an entire annotation from a docblock.
*
* @author Graham Campbell <hello@gjcampbell.co.uk>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
*/
final class Annotation implements \Stringable
{
/**
* All the annotation tag names with types.
*
* @var non-empty-list<string>
*/
public const TAGS_WITH_TYPES = [
'extends',
'implements',
'method',
'param',
'param-out',
'phpstan-type',
'phpstan-import-type',
'property',
'property-read',
'property-write',
'psalm-type',
'psalm-import-type',
'return',
'throws',
'type',
'var',
];
/**
* The lines that make up the annotation.
*
* @var non-empty-list<Line>
*/
private array $lines;
/**
* The position of the first line of the annotation in the docblock.
*/
private int $start;
/**
* The position of the last line of the annotation in the docblock.
*/
private int $end;
/**
* The associated tag.
*/
private ?Tag $tag = null;
/**
* Lazy loaded, cached types content.
*/
private ?string $typesContent = null;
/**
* The cached types.
*
* @var null|list<string>
*/
private ?array $types = null;
private ?NamespaceAnalysis $namespace = null;
/**
* @var list<NamespaceUseAnalysis>
*/
private array $namespaceUses;
/**
* Create a new line instance.
*
* @param non-empty-array<int, Line> $lines
* @param null|NamespaceAnalysis $namespace
* @param list<NamespaceUseAnalysis> $namespaceUses
*/
public function __construct(array $lines, $namespace = null, array $namespaceUses = [])
{
$this->lines = array_values($lines);
$this->namespace = $namespace;
$this->namespaceUses = $namespaceUses;
$this->start = array_key_first($lines);
$this->end = array_key_last($lines);
}
/**
* Get the string representation of object.
*/
public function __toString(): string
{
return $this->getContent();
}
/**
* Get all the annotation tag names with types.
*
* @return non-empty-list<string>
*
* @deprecated Use `Annotation::TAGS_WITH_TYPES` constant instead
*
* @TODO 4.0 remove me
*/
public static function getTagsWithTypes(): array
{
return self::TAGS_WITH_TYPES;
}
/**
* Get the start position of this annotation.
*/
public function getStart(): int
{
return $this->start;
}
/**
* Get the end position of this annotation.
*/
public function getEnd(): int
{
return $this->end;
}
/**
* Get the associated tag.
*/
public function getTag(): Tag
{
if (null === $this->tag) {
$this->tag = new Tag($this->lines[0]);
}
return $this->tag;
}
/**
* @internal
*/
public function getTypeExpression(): ?TypeExpression
{
$typesContent = $this->getTypesContent();
return null === $typesContent
? null
: new TypeExpression($typesContent, $this->namespace, $this->namespaceUses);
}
/**
* @internal
*/
public function getVariableName(): ?string
{
$type = preg_quote($this->getTypesContent() ?? '', '/');
$regex = \sprintf(
'/@%s\s+(%s\s*)?(&\s*)?(\.{3}\s*)?(?<variable>\$%s)(?:.*|$)/',
$this->tag->getName(),
$type,
TypeExpression::REGEX_IDENTIFIER,
);
if (Preg::match($regex, $this->getContent(), $matches)) {
\assert(isset($matches['variable']));
return $matches['variable'];
}
return null;
}
/**
* Get the types associated with this annotation.
*
* @return list<string>
*/
public function getTypes(): array
{
if (null === $this->types) {
$typeExpression = $this->getTypeExpression();
$this->types = null === $typeExpression
? []
: $typeExpression->getTypes();
}
return $this->types;
}
/**
* Set the types associated with this annotation.
*
* @param list<string> $types
*/
public function setTypes(array $types): void
{
$origTypesContent = $this->getTypesContent();
$newTypesContent = implode(
// Fallback to union type is provided for backward compatibility (previously glue was set to `|` by default even when type was not composite)
// @TODO Better handling for cases where type is fixed (original type is not composite, but was made composite during fix)
$this->getTypeExpression()->getTypesGlue() ?? '|',
$types,
);
if ($origTypesContent === $newTypesContent) {
return;
}
$originalTypesLines = Preg::split('/([^\n\r]+\R*)/', $origTypesContent, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE);
$newTypesLines = Preg::split('/([^\n\r]+\R*)/', $newTypesContent, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE);
\assert(\count($originalTypesLines) === \count($newTypesLines));
foreach ($newTypesLines as $index => $line) {
\assert(isset($originalTypesLines[$index]));
$pattern = '/'.preg_quote($originalTypesLines[$index], '/').'/';
\assert(isset($this->lines[$index]));
$this->lines[$index]->setContent(Preg::replace($pattern, $line, $this->lines[$index]->getContent(), 1));
}
$this->clearCache();
}
/**
* Get the normalized types associated with this annotation, so they can easily be compared.
*
* @return list<string>
*/
public function getNormalizedTypes(): array
{
$typeExpression = $this->getTypeExpression();
if (null === $typeExpression) {
return [];
}
$normalizedTypeExpression = $typeExpression
->mapTypes(static fn (TypeExpression $v) => new TypeExpression(strtolower($v->toString()), null, []))
->sortTypes(static fn (TypeExpression $a, TypeExpression $b) => $a->toString() <=> $b->toString())
;
return $normalizedTypeExpression->getTypes();
}
/**
* Remove this annotation by removing all its lines.
*/
public function remove(): void
{
foreach ($this->lines as $line) {
if ($line->isTheStart() && $line->isTheEnd()) {
// Single line doc block, remove entirely
$line->remove();
} elseif ($line->isTheStart()) {
// Multi line doc block, but start is on the same line as the first annotation, keep only the start
$content = Preg::replace('#(\s*/\*\*).*#', '$1', $line->getContent());
$line->setContent($content);
} elseif ($line->isTheEnd()) {
// Multi line doc block, but end is on the same line as the last annotation, keep only the end
$content = Preg::replace('#(\s*)\S.*(\*/.*)#', '$1$2', $line->getContent());
$line->setContent($content);
} else {
// Multi line doc block, neither start nor end on this line, can be removed safely
$line->remove();
}
}
$this->clearCache();
}
/**
* Get the annotation content.
*/
public function getContent(): string
{
return implode('', $this->lines);
}
public function supportTypes(): bool
{
return \in_array($this->getTag()->getName(), self::TAGS_WITH_TYPES, true);
}
/**
* Get the current types content.
*
* Be careful modifying the underlying line as that won't flush the cache.
*/
private function getTypesContent(): ?string
{
if (null === $this->typesContent) {
$name = $this->getTag()->getName();
if (!$this->supportTypes()) {
throw new \RuntimeException('This tag does not support types.');
}
if (Preg::match(
'{^(?:\h*\*|/\*\*)[\h*]*@'.$name.'\h+'.TypeExpression::REGEX_TYPES.'(?:(?:[*\h\v]|\&?[\.\$\s]).*)?\r?$}is',
$this->getContent(),
$matches,
)) {
\assert(isset($matches['types']));
$this->typesContent = $matches['types'];
}
}
return $this->typesContent;
}
private function clearCache(): void
{
$this->types = null;
$this->typesContent = null;
}
}