Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
tipuloidea
/
back
/
vendor
/
friendsofphp
/
php-cs-fixer
/
src
/
DocBlock
:
Annotation.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?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; } }