Missing dependancies

This commit is contained in:
tokslaw7
2023-06-21 12:19:22 +00:00
parent fbf3f180c2
commit 421f25c80d
2356 changed files with 342670 additions and 4743 deletions
@@ -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',
];
}
}