Missing dependancies
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
<?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>
|
||||
*/
|
||||
final class Annotation
|
||||
{
|
||||
/**
|
||||
* All the annotation tag names with types.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private static array $tags = [
|
||||
'method',
|
||||
'param',
|
||||
'property',
|
||||
'property-read',
|
||||
'property-write',
|
||||
'return',
|
||||
'throws',
|
||||
'type',
|
||||
'var',
|
||||
];
|
||||
|
||||
/**
|
||||
* The lines that make up the annotation.
|
||||
*
|
||||
* @var Line[]
|
||||
*/
|
||||
private array $lines;
|
||||
|
||||
/**
|
||||
* The position of the first line of the annotation in the docblock.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $start;
|
||||
|
||||
/**
|
||||
* The position of the last line of the annotation in the docblock.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $end;
|
||||
|
||||
/**
|
||||
* The associated tag.
|
||||
*
|
||||
* @var null|Tag
|
||||
*/
|
||||
private $tag;
|
||||
|
||||
/**
|
||||
* Lazy loaded, cached types content.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
private $typesContent;
|
||||
|
||||
/**
|
||||
* The cached types.
|
||||
*
|
||||
* @var null|string[]
|
||||
*/
|
||||
private $types;
|
||||
|
||||
/**
|
||||
* @var null|NamespaceAnalysis
|
||||
*/
|
||||
private $namespace;
|
||||
|
||||
/**
|
||||
* @var NamespaceUseAnalysis[]
|
||||
*/
|
||||
private array $namespaceUses;
|
||||
|
||||
/**
|
||||
* Create a new line instance.
|
||||
*
|
||||
* @param Line[] $lines
|
||||
* @param null|NamespaceAnalysis $namespace
|
||||
* @param NamespaceUseAnalysis[] $namespaceUses
|
||||
*/
|
||||
public function __construct(array $lines, $namespace = null, array $namespaceUses = [])
|
||||
{
|
||||
$this->lines = array_values($lines);
|
||||
$this->namespace = $namespace;
|
||||
$this->namespaceUses = $namespaceUses;
|
||||
|
||||
$keys = array_keys($lines);
|
||||
|
||||
$this->start = $keys[0];
|
||||
$this->end = end($keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string representation of object.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the annotation tag names with types.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function getTagsWithTypes(): array
|
||||
{
|
||||
return self::$tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
return new TypeExpression($this->getTypesContent(), $this->namespace, $this->namespaceUses);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|string
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getVariableName()
|
||||
{
|
||||
$type = preg_quote($this->getTypesContent(), '/');
|
||||
$regex = "/@{$this->tag->getName()}\\s+({$type}\\s*)?(&\\s*)?(\\.{3}\\s*)?(?<variable>\\$.+?)(?:[\\s*]|$)/";
|
||||
|
||||
if (Preg::match($regex, $this->lines[0]->getContent(), $matches)) {
|
||||
return $matches['variable'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the types associated with this annotation.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getTypes(): array
|
||||
{
|
||||
if (null === $this->types) {
|
||||
$this->types = $this->getTypeExpression()->getTypes();
|
||||
}
|
||||
|
||||
return $this->types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the types associated with this annotation.
|
||||
*
|
||||
* @param string[] $types
|
||||
*/
|
||||
public function setTypes(array $types): void
|
||||
{
|
||||
$pattern = '/'.preg_quote($this->getTypesContent(), '/').'/';
|
||||
|
||||
$this->lines[0]->setContent(Preg::replace($pattern, implode($this->getTypeExpression()->getTypesGlue(), $types), $this->lines[0]->getContent(), 1));
|
||||
|
||||
$this->clearCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the normalized types associated with this annotation, so they can easily be compared.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getNormalizedTypes(): array
|
||||
{
|
||||
$normalized = array_map(static function (string $type): string {
|
||||
return strtolower($type);
|
||||
}, $this->getTypes());
|
||||
|
||||
sort($normalized);
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, 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.');
|
||||
}
|
||||
|
||||
$matchingResult = Preg::match(
|
||||
'{^(?:\s*\*|/\*\*)\s*@'.$name.'\s+'.TypeExpression::REGEX_TYPES.'(?:(?:[*\h\v]|\&[\.\$]).*)?\r?$}isx',
|
||||
$this->lines[0]->getContent(),
|
||||
$matches
|
||||
);
|
||||
|
||||
$this->typesContent = 1 === $matchingResult
|
||||
? $matches['types']
|
||||
: '';
|
||||
}
|
||||
|
||||
return $this->typesContent;
|
||||
}
|
||||
|
||||
private function clearCache(): void
|
||||
{
|
||||
$this->types = null;
|
||||
$this->typesContent = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
<?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 class represents a docblock.
|
||||
*
|
||||
* It internally splits it up into "lines" that we can manipulate.
|
||||
*
|
||||
* @author Graham Campbell <hello@gjcampbell.co.uk>
|
||||
*/
|
||||
final class DocBlock
|
||||
{
|
||||
/**
|
||||
* @var list<Line>
|
||||
*/
|
||||
private array $lines = [];
|
||||
|
||||
/**
|
||||
* @var null|list<Annotation>
|
||||
*/
|
||||
private ?array $annotations = null;
|
||||
|
||||
private ?NamespaceAnalysis $namespace;
|
||||
|
||||
/**
|
||||
* @var list<NamespaceUseAnalysis>
|
||||
*/
|
||||
private array $namespaceUses;
|
||||
|
||||
/**
|
||||
* @param list<NamespaceUseAnalysis> $namespaceUses
|
||||
*/
|
||||
public function __construct(string $content, ?NamespaceAnalysis $namespace = null, array $namespaceUses = [])
|
||||
{
|
||||
foreach (Preg::split('/([^\n\r]+\R*)/', $content, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $line) {
|
||||
$this->lines[] = new Line($line);
|
||||
}
|
||||
|
||||
$this->namespace = $namespace;
|
||||
$this->namespaceUses = $namespaceUses;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this docblock's lines.
|
||||
*
|
||||
* @return list<Line>
|
||||
*/
|
||||
public function getLines(): array
|
||||
{
|
||||
return $this->lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single line.
|
||||
*/
|
||||
public function getLine(int $pos): ?Line
|
||||
{
|
||||
return $this->lines[$pos] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this docblock's annotations.
|
||||
*
|
||||
* @return list<Annotation>
|
||||
*/
|
||||
public function getAnnotations(): array
|
||||
{
|
||||
if (null !== $this->annotations) {
|
||||
return $this->annotations;
|
||||
}
|
||||
|
||||
$this->annotations = [];
|
||||
$total = \count($this->lines);
|
||||
|
||||
for ($index = 0; $index < $total; ++$index) {
|
||||
if ($this->lines[$index]->containsATag()) {
|
||||
// get all the lines that make up the annotation
|
||||
$lines = \array_slice($this->lines, $index, $this->findAnnotationLength($index), true);
|
||||
$annotation = new Annotation($lines, $this->namespace, $this->namespaceUses);
|
||||
// move the index to the end of the annotation to avoid
|
||||
// checking it again because we know the lines inside the
|
||||
// current annotation cannot be part of another annotation
|
||||
$index = $annotation->getEnd();
|
||||
// add the current annotation to the list of annotations
|
||||
$this->annotations[] = $annotation;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->annotations;
|
||||
}
|
||||
|
||||
public function isMultiLine(): bool
|
||||
{
|
||||
return 1 !== \count($this->lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a one line doc block, and turn it into a multi line doc block.
|
||||
*/
|
||||
public function makeMultiLine(string $indent, string $lineEnd): void
|
||||
{
|
||||
if ($this->isMultiLine()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lineContent = $this->getSingleLineDocBlockEntry($this->lines[0]);
|
||||
|
||||
if ('' === $lineContent) {
|
||||
$this->lines = [
|
||||
new Line('/**'.$lineEnd),
|
||||
new Line($indent.' *'.$lineEnd),
|
||||
new Line($indent.' */'),
|
||||
];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->lines = [
|
||||
new Line('/**'.$lineEnd),
|
||||
new Line($indent.' * '.$lineContent.$lineEnd),
|
||||
new Line($indent.' */'),
|
||||
];
|
||||
}
|
||||
|
||||
public function makeSingleLine(): void
|
||||
{
|
||||
if (!$this->isMultiLine()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$usefulLines = array_filter(
|
||||
$this->lines,
|
||||
static function (Line $line): bool {
|
||||
return $line->containsUsefulContent();
|
||||
}
|
||||
);
|
||||
|
||||
if (1 < \count($usefulLines)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lineContent = '';
|
||||
if (\count($usefulLines) > 0) {
|
||||
$lineContent = $this->getSingleLineDocBlockEntry(array_shift($usefulLines));
|
||||
}
|
||||
|
||||
$this->lines = [new Line('/** '.$lineContent.' */')];
|
||||
}
|
||||
|
||||
public function getAnnotation(int $pos): ?Annotation
|
||||
{
|
||||
$annotations = $this->getAnnotations();
|
||||
|
||||
return $annotations[$pos] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific types of annotations only.
|
||||
*
|
||||
* @param list<string>|string $types
|
||||
*
|
||||
* @return list<Annotation>
|
||||
*/
|
||||
public function getAnnotationsOfType($types): array
|
||||
{
|
||||
$typesToSearchFor = (array) $types;
|
||||
|
||||
$annotations = [];
|
||||
|
||||
foreach ($this->getAnnotations() as $annotation) {
|
||||
$tagName = $annotation->getTag()->getName();
|
||||
if (\in_array($tagName, $typesToSearchFor, true)) {
|
||||
$annotations[] = $annotation;
|
||||
}
|
||||
}
|
||||
|
||||
return $annotations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actual content of this docblock.
|
||||
*/
|
||||
public function getContent(): string
|
||||
{
|
||||
return implode('', $this->lines);
|
||||
}
|
||||
|
||||
private function findAnnotationLength(int $start): int
|
||||
{
|
||||
$index = $start;
|
||||
|
||||
while ($line = $this->getLine(++$index)) {
|
||||
if ($line->containsATag()) {
|
||||
// we've 100% reached the end of the description if we get here
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$line->containsUsefulContent()) {
|
||||
// if next line is also non-useful, or contains a tag, then we're done here
|
||||
$next = $this->getLine($index + 1);
|
||||
if (null === $next || !$next->containsUsefulContent() || $next->containsATag()) {
|
||||
break;
|
||||
}
|
||||
// otherwise, continue, the annotation must have contained a blank line in its description
|
||||
}
|
||||
}
|
||||
|
||||
return $index - $start;
|
||||
}
|
||||
|
||||
private function getSingleLineDocBlockEntry(Line $line): string
|
||||
{
|
||||
$lineString = $line->getContent();
|
||||
|
||||
if ('' === $lineString) {
|
||||
return $lineString;
|
||||
}
|
||||
|
||||
$lineString = str_replace('*/', '', $lineString);
|
||||
$lineString = trim($lineString);
|
||||
|
||||
if (str_starts_with($lineString, '/**')) {
|
||||
$lineString = substr($lineString, 3);
|
||||
} elseif (str_starts_with($lineString, '*')) {
|
||||
$lineString = substr($lineString, 1);
|
||||
}
|
||||
|
||||
return trim($lineString);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* This represents a line of a docblock.
|
||||
*
|
||||
* @author Graham Campbell <hello@gjcampbell.co.uk>
|
||||
*/
|
||||
final class Line
|
||||
{
|
||||
/**
|
||||
* The content of this line.
|
||||
*/
|
||||
private string $content;
|
||||
|
||||
/**
|
||||
* Create a new line instance.
|
||||
*/
|
||||
public function __construct(string $content)
|
||||
{
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string representation of object.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content of this line.
|
||||
*/
|
||||
public function getContent(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this line contain useful content?
|
||||
*
|
||||
* If the line contains text or tags, then this is true.
|
||||
*/
|
||||
public function containsUsefulContent(): bool
|
||||
{
|
||||
return 0 !== Preg::match('/\\*\s*\S+/', $this->content) && '' !== trim(str_replace(['/', '*'], ' ', $this->content));
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the line contain a tag?
|
||||
*
|
||||
* If this is true, then it must be the first line of an annotation.
|
||||
*/
|
||||
public function containsATag(): bool
|
||||
{
|
||||
return 0 !== Preg::match('/\\*\s*@/', $this->content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the line the start of a docblock?
|
||||
*/
|
||||
public function isTheStart(): bool
|
||||
{
|
||||
return str_contains($this->content, '/**');
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the line the end of a docblock?
|
||||
*/
|
||||
public function isTheEnd(): bool
|
||||
{
|
||||
return str_contains($this->content, '*/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the content of this line.
|
||||
*/
|
||||
public function setContent(string $content): void
|
||||
{
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this line by clearing its contents.
|
||||
*
|
||||
* Note that this method technically brakes the internal state of the
|
||||
* docblock, but is useful when we need to retain the indices of lines
|
||||
* during the execution of an algorithm.
|
||||
*/
|
||||
public function remove(): void
|
||||
{
|
||||
$this->content = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a blank docblock line to this line's contents.
|
||||
*
|
||||
* Note that this method technically brakes the internal state of the
|
||||
* docblock, but is useful when we need to retain the indices of lines
|
||||
* during the execution of an algorithm.
|
||||
*/
|
||||
public function addBlank(): void
|
||||
{
|
||||
$matched = Preg::match('/^(\h*\*)[^\r\n]*(\r?\n)$/', $this->content, $matches);
|
||||
|
||||
if (1 !== $matched) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->content .= $matches[1].$matches[2];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* This class represents a short description (aka summary) of a docblock.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class ShortDescription
|
||||
{
|
||||
/**
|
||||
* The docblock containing the short description.
|
||||
*/
|
||||
private DocBlock $doc;
|
||||
|
||||
public function __construct(DocBlock $doc)
|
||||
{
|
||||
$this->doc = $doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the line index of the line containing the end of the short
|
||||
* description, if present.
|
||||
*/
|
||||
public function getEnd(): ?int
|
||||
{
|
||||
$reachedContent = false;
|
||||
|
||||
foreach ($this->doc->getLines() as $index => $line) {
|
||||
// we went past a description, then hit a tag or blank line, so
|
||||
// the last line of the description must be the one before this one
|
||||
if ($reachedContent && ($line->containsATag() || !$line->containsUsefulContent())) {
|
||||
return $index - 1;
|
||||
}
|
||||
|
||||
// no short description was found
|
||||
if ($line->containsATag()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// we've reached content, but need to check the next lines too
|
||||
// in case the short description is multi-line
|
||||
if ($line->containsUsefulContent()) {
|
||||
$reachedContent = true;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* This represents a tag, as defined by the proposed PSR PHPDoc standard.
|
||||
*
|
||||
* @author Graham Campbell <hello@gjcampbell.co.uk>
|
||||
* @author Jakub Kwaśniewski <jakub@zero-85.pl>
|
||||
*/
|
||||
final class Tag
|
||||
{
|
||||
/**
|
||||
* All the tags defined by the proposed PSR PHPDoc standard.
|
||||
*/
|
||||
public const PSR_STANDARD_TAGS = [
|
||||
'api', 'author', 'category', 'copyright', 'deprecated', 'example',
|
||||
'global', 'internal', 'license', 'link', 'method', 'package', 'param',
|
||||
'property', 'property-read', 'property-write', 'return', 'see',
|
||||
'since', 'subpackage', 'throws', 'todo', 'uses', 'var', 'version',
|
||||
];
|
||||
|
||||
/**
|
||||
* The line containing the tag.
|
||||
*/
|
||||
private Line $line;
|
||||
|
||||
/**
|
||||
* The cached tag name.
|
||||
*/
|
||||
private ?string $name = null;
|
||||
|
||||
/**
|
||||
* Create a new tag instance.
|
||||
*/
|
||||
public function __construct(Line $line)
|
||||
{
|
||||
$this->line = $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tag name.
|
||||
*
|
||||
* This may be "param", or "return", etc.
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
if (null === $this->name) {
|
||||
Preg::matchAll('/@[a-zA-Z0-9_-]+(?=\s|$)/', $this->line->getContent(), $matches);
|
||||
|
||||
if (isset($matches[0][0])) {
|
||||
$this->name = ltrim($matches[0][0], '@');
|
||||
} else {
|
||||
$this->name = 'other';
|
||||
}
|
||||
}
|
||||
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tag name.
|
||||
*
|
||||
* This will also be persisted to the upstream line and annotation.
|
||||
*/
|
||||
public function setName(string $name): void
|
||||
{
|
||||
$current = $this->getName();
|
||||
|
||||
if ('other' === $current) {
|
||||
throw new \RuntimeException('Cannot set name on unknown tag.');
|
||||
}
|
||||
|
||||
$this->line->setContent(Preg::replace("/@{$current}/", "@{$name}", $this->line->getContent(), 1));
|
||||
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the tag a known tag?
|
||||
*
|
||||
* This is defined by if it exists in the proposed PSR PHPDoc standard.
|
||||
*/
|
||||
public function valid(): bool
|
||||
{
|
||||
return \in_array($this->getName(), self::PSR_STANDARD_TAGS, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* This class is responsible for comparing tags to see if they should be kept
|
||||
* together, or kept apart.
|
||||
*
|
||||
* @author Graham Campbell <hello@gjcampbell.co.uk>
|
||||
* @author Jakub Kwaśniewski <jakub@zero-85.pl>
|
||||
*/
|
||||
final class TagComparator
|
||||
{
|
||||
/**
|
||||
* Groups of tags that should be allowed to immediately follow each other.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public const DEFAULT_GROUPS = [
|
||||
['deprecated', 'link', 'see', 'since'],
|
||||
['author', 'copyright', 'license'],
|
||||
['category', 'package', 'subpackage'],
|
||||
['property', 'property-read', 'property-write'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Should the given tags be kept together, or kept apart?
|
||||
*
|
||||
* @param string[][] $groups
|
||||
*/
|
||||
public static function shouldBeTogether(Tag $first, Tag $second, array $groups = self::DEFAULT_GROUPS): bool
|
||||
{
|
||||
$firstName = $first->getName();
|
||||
$secondName = $second->getName();
|
||||
|
||||
if ($firstName === $secondName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($groups as $group) {
|
||||
if (\in_array($firstName, $group, true) && \in_array($secondName, $group, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
<?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;
|
||||
use PhpCsFixer\Utils;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class TypeExpression
|
||||
{
|
||||
/**
|
||||
* Regex to match any types, shall be used with `x` modifier.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public const REGEX_TYPES = '
|
||||
(?<types> # several types separated by `|` or `&`
|
||||
(?<type> # single type
|
||||
(?<nullable>\??)
|
||||
(?:
|
||||
(?<object_like_array>
|
||||
(?<object_like_array_start>array\h*\{)
|
||||
(?<object_like_array_keys>
|
||||
(?<object_like_array_key>
|
||||
\h*[^?:\h]+\h*\??\h*:\h*(?&types)
|
||||
)
|
||||
(?:\h*,(?&object_like_array_key))*
|
||||
)
|
||||
\h*\}
|
||||
)
|
||||
|
|
||||
(?<callable> # callable syntax, e.g. `callable(string): bool`
|
||||
(?<callable_start>(?:callable|Closure)\h*\(\h*)
|
||||
(?<callable_arguments>
|
||||
(?&types)
|
||||
(?:
|
||||
\h*,\h*
|
||||
(?&types)
|
||||
)*
|
||||
)?
|
||||
\h*\)
|
||||
(?:
|
||||
\h*\:\h*
|
||||
(?<callable_return>(?&types))
|
||||
)?
|
||||
)
|
||||
|
|
||||
(?<generic> # generic syntax, e.g.: `array<int, \Foo\Bar>`
|
||||
(?<generic_start>
|
||||
(?&name)+
|
||||
\h*<\h*
|
||||
)
|
||||
(?<generic_types>
|
||||
(?&types)
|
||||
(?:
|
||||
\h*,\h*
|
||||
(?&types)
|
||||
)*
|
||||
)
|
||||
\h*>
|
||||
)
|
||||
|
|
||||
(?<class_constant> # class constants with optional wildcard, e.g.: `Foo::*`, `Foo::CONST_A`, `FOO::CONST_*`
|
||||
(?&name)::(\*|\w+\*?)
|
||||
)
|
||||
|
|
||||
(?<array> # array expression, e.g.: `string[]`, `string[][]`
|
||||
(?&name)(\[\])+
|
||||
)
|
||||
|
|
||||
(?<constant> # single constant value (case insensitive), e.g.: 1, `\'a\'`
|
||||
(?i)
|
||||
null | true | false
|
||||
| -?(?:\d+(?:\.\d*)?|\.\d+) # all sorts of numbers with or without minus, e.g.: 1, 1.1, 1., .1, -1
|
||||
| \'[^\']+?\' | "[^"]+?"
|
||||
| [@$]?(?:this | self | static)
|
||||
(?-i)
|
||||
)
|
||||
|
|
||||
(?<name> # single type, e.g.: `null`, `int`, `\Foo\Bar`
|
||||
[\\\\\w-]++
|
||||
)
|
||||
)
|
||||
)
|
||||
(?:
|
||||
\h*(?<glue>[|&])\h*
|
||||
(?&type)
|
||||
)*
|
||||
)
|
||||
';
|
||||
|
||||
private string $value;
|
||||
|
||||
private bool $isUnionType = false;
|
||||
|
||||
/**
|
||||
* @var list<array{start_index: int, expression: self}>
|
||||
*/
|
||||
private array $innerTypeExpressions = [];
|
||||
|
||||
private string $typesGlue = '|';
|
||||
|
||||
private ?NamespaceAnalysis $namespace;
|
||||
|
||||
/**
|
||||
* @var NamespaceUseAnalysis[]
|
||||
*/
|
||||
private array $namespaceUses;
|
||||
|
||||
/**
|
||||
* @param NamespaceUseAnalysis[] $namespaceUses
|
||||
*/
|
||||
public function __construct(string $value, ?NamespaceAnalysis $namespace, array $namespaceUses)
|
||||
{
|
||||
$this->value = $value;
|
||||
$this->namespace = $namespace;
|
||||
$this->namespaceUses = $namespaceUses;
|
||||
|
||||
$this->parse();
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getTypes(): array
|
||||
{
|
||||
if ($this->isUnionType) {
|
||||
return array_map(
|
||||
static fn (array $type) => $type['expression']->toString(),
|
||||
$this->innerTypeExpressions,
|
||||
);
|
||||
}
|
||||
|
||||
return [$this->value];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(self $a, self $b): int $compareCallback
|
||||
*/
|
||||
public function sortTypes(callable $compareCallback): void
|
||||
{
|
||||
foreach (array_reverse($this->innerTypeExpressions) as [
|
||||
'start_index' => $startIndex,
|
||||
'expression' => $inner,
|
||||
]) {
|
||||
$initialValueLength = \strlen($inner->toString());
|
||||
|
||||
$inner->sortTypes($compareCallback);
|
||||
|
||||
$this->value = substr_replace(
|
||||
$this->value,
|
||||
$inner->toString(),
|
||||
$startIndex,
|
||||
$initialValueLength
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->isUnionType) {
|
||||
$this->innerTypeExpressions = Utils::stableSort(
|
||||
$this->innerTypeExpressions,
|
||||
static fn (array $type): self => $type['expression'],
|
||||
$compareCallback,
|
||||
);
|
||||
|
||||
$this->value = implode($this->getTypesGlue(), $this->getTypes());
|
||||
}
|
||||
}
|
||||
|
||||
public function getTypesGlue(): string
|
||||
{
|
||||
return $this->typesGlue;
|
||||
}
|
||||
|
||||
public function getCommonType(): ?string
|
||||
{
|
||||
$aliases = $this->getAliases();
|
||||
|
||||
$mainType = null;
|
||||
|
||||
foreach ($this->getTypes() as $type) {
|
||||
if ('null' === $type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($aliases[$type])) {
|
||||
$type = $aliases[$type];
|
||||
} elseif (1 === Preg::match('/\[\]$/', $type)) {
|
||||
$type = 'array';
|
||||
} elseif (1 === Preg::match('/^(.+?)</', $type, $matches)) {
|
||||
$type = $matches[1];
|
||||
}
|
||||
|
||||
if (null === $mainType || $type === $mainType) {
|
||||
$mainType = $type;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$mainType = $this->getParentType($type, $mainType);
|
||||
|
||||
if (null === $mainType) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $mainType;
|
||||
}
|
||||
|
||||
public function allowsNull(): bool
|
||||
{
|
||||
foreach ($this->getTypes() as $type) {
|
||||
if (\in_array($type, ['null', 'mixed'], true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function parse(): void
|
||||
{
|
||||
$value = $this->value;
|
||||
|
||||
Preg::match(
|
||||
'{^'.self::REGEX_TYPES.'$}x',
|
||||
$value,
|
||||
$matches
|
||||
);
|
||||
|
||||
if ([] === $matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->typesGlue = $matches['glue'] ?? $this->typesGlue;
|
||||
|
||||
$index = '' !== $matches['nullable'] ? 1 : 0;
|
||||
|
||||
if ($matches['type'] !== $matches['types']) {
|
||||
$this->isUnionType = true;
|
||||
|
||||
while (true) {
|
||||
$innerType = $matches['type'];
|
||||
|
||||
$newValue = Preg::replace(
|
||||
'/^'.preg_quote($innerType, '/').'(\h*[|&]\h*)?/',
|
||||
'',
|
||||
$value
|
||||
);
|
||||
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => $index,
|
||||
'expression' => $this->inner($innerType),
|
||||
];
|
||||
|
||||
if ('' === $newValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
$index += \strlen($value) - \strlen($newValue);
|
||||
$value = $newValue;
|
||||
|
||||
Preg::match(
|
||||
'{^'.self::REGEX_TYPES.'$}x',
|
||||
$value,
|
||||
$matches
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ('' !== ($matches['generic'] ?? '')) {
|
||||
$this->parseCommaSeparatedInnerTypes(
|
||||
$index + \strlen($matches['generic_start']),
|
||||
$matches['generic_types']
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ('' !== ($matches['callable'] ?? '')) {
|
||||
$this->parseCommaSeparatedInnerTypes(
|
||||
$index + \strlen($matches['callable_start']),
|
||||
$matches['callable_arguments'] ?? ''
|
||||
);
|
||||
|
||||
$return = $matches['callable_return'] ?? null;
|
||||
if (null !== $return) {
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => \strlen($this->value) - \strlen($matches['callable_return']),
|
||||
'expression' => $this->inner($matches['callable_return']),
|
||||
];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ('' !== ($matches['object_like_array'] ?? '')) {
|
||||
$this->parseObjectLikeArrayKeys(
|
||||
$index + \strlen($matches['object_like_array_start']),
|
||||
$matches['object_like_array_keys']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function parseCommaSeparatedInnerTypes(int $startIndex, string $value): void
|
||||
{
|
||||
while ('' !== $value) {
|
||||
Preg::match(
|
||||
'{^'.self::REGEX_TYPES.'\h*(?:,|$)}x',
|
||||
$value,
|
||||
$matches
|
||||
);
|
||||
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => $startIndex,
|
||||
'expression' => $this->inner($matches['types']),
|
||||
];
|
||||
|
||||
$newValue = Preg::replace(
|
||||
'/^'.preg_quote($matches['types'], '/').'(\h*\,\h*)?/',
|
||||
'',
|
||||
$value
|
||||
);
|
||||
|
||||
$startIndex += \strlen($value) - \strlen($newValue);
|
||||
$value = $newValue;
|
||||
}
|
||||
}
|
||||
|
||||
private function parseObjectLikeArrayKeys(int $startIndex, string $value): void
|
||||
{
|
||||
while ('' !== $value) {
|
||||
Preg::match(
|
||||
'{(?<_start>^.+?:\h*)'.self::REGEX_TYPES.'\h*(?:,|$)}x',
|
||||
$value,
|
||||
$matches
|
||||
);
|
||||
|
||||
$this->innerTypeExpressions[] = [
|
||||
'start_index' => $startIndex + \strlen($matches['_start']),
|
||||
'expression' => $this->inner($matches['types']),
|
||||
];
|
||||
|
||||
$newValue = Preg::replace(
|
||||
'/^.+?:\h*'.preg_quote($matches['types'], '/').'(\h*\,\h*)?/',
|
||||
'',
|
||||
$value
|
||||
);
|
||||
|
||||
$startIndex += \strlen($value) - \strlen($newValue);
|
||||
$value = $newValue;
|
||||
}
|
||||
}
|
||||
|
||||
private function inner(string $value): self
|
||||
{
|
||||
return new self($value, $this->namespace, $this->namespaceUses);
|
||||
}
|
||||
|
||||
private function getParentType(string $type1, string $type2): ?string
|
||||
{
|
||||
$types = [
|
||||
$this->normalize($type1),
|
||||
$this->normalize($type2),
|
||||
];
|
||||
natcasesort($types);
|
||||
$types = implode('|', $types);
|
||||
|
||||
$parents = [
|
||||
'array|Traversable' => 'iterable',
|
||||
'array|iterable' => 'iterable',
|
||||
'iterable|Traversable' => 'iterable',
|
||||
'self|static' => 'self',
|
||||
];
|
||||
|
||||
return $parents[$types] ?? null;
|
||||
}
|
||||
|
||||
private function normalize(string $type): string
|
||||
{
|
||||
$aliases = $this->getAliases();
|
||||
|
||||
if (isset($aliases[$type])) {
|
||||
return $aliases[$type];
|
||||
}
|
||||
|
||||
if (\in_array($type, [
|
||||
'array',
|
||||
'bool',
|
||||
'callable',
|
||||
'float',
|
||||
'int',
|
||||
'iterable',
|
||||
'mixed',
|
||||
'never',
|
||||
'null',
|
||||
'object',
|
||||
'resource',
|
||||
'string',
|
||||
'void',
|
||||
], true)) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
if (1 === Preg::match('/\[\]$/', $type)) {
|
||||
return 'array';
|
||||
}
|
||||
|
||||
if (1 === Preg::match('/^(.+?)</', $type, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
if (str_starts_with($type, '\\')) {
|
||||
return substr($type, 1);
|
||||
}
|
||||
|
||||
foreach ($this->namespaceUses as $namespaceUse) {
|
||||
if ($namespaceUse->getShortName() === $type) {
|
||||
return $namespaceUse->getFullName();
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $this->namespace || $this->namespace->isGlobalNamespace()) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
return "{$this->namespace->getFullName()}\\{$type}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,string>
|
||||
*/
|
||||
private function getAliases(): array
|
||||
{
|
||||
return [
|
||||
'boolean' => 'bool',
|
||||
'callback' => 'callable',
|
||||
'double' => 'float',
|
||||
'false' => 'bool',
|
||||
'integer' => 'int',
|
||||
'real' => 'float',
|
||||
'true' => 'bool',
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user