|
@@ -17,6 +17,7 @@ namespace PhpCsFixer\DocBlock;
|
|
|
use PhpCsFixer\Preg;
|
|
|
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
|
|
|
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
|
|
|
+use PhpCsFixer\Utils;
|
|
|
|
|
|
/**
|
|
|
* @internal
|
|
@@ -31,39 +32,47 @@ final class TypeExpression
|
|
|
public const REGEX_TYPES = '
|
|
|
(?<types> # several types separated by `|` or `&`
|
|
|
(?<type> # single type
|
|
|
- \?? # optionally nullable
|
|
|
+ (?<nullable>\??)
|
|
|
(?:
|
|
|
(?<object_like_array>
|
|
|
- array\h*\{
|
|
|
- (?<object_like_array_key>
|
|
|
- \h*[^?:\h]+\h*\??\h*:\h*(?&types)
|
|
|
+ (?<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*,(?&object_like_array_key))*
|
|
|
\h*\}
|
|
|
)
|
|
|
|
|
|
|
(?<callable> # callable syntax, e.g. `callable(string): bool`
|
|
|
- (?:callable|Closure)\h*\(\h*
|
|
|
- (?&types)
|
|
|
- (?:
|
|
|
- \h*,\h*
|
|
|
+ (?<callable_start>(?:callable|Closure)\h*\(\h*)
|
|
|
+ (?<callable_arguments>
|
|
|
(?&types)
|
|
|
- )*
|
|
|
+ (?:
|
|
|
+ \h*,\h*
|
|
|
+ (?&types)
|
|
|
+ )*
|
|
|
+ )?
|
|
|
\h*\)
|
|
|
(?:
|
|
|
\h*\:\h*
|
|
|
- (?&types)
|
|
|
+ (?<callable_return>(?&types))
|
|
|
)?
|
|
|
)
|
|
|
|
|
|
|
(?<generic> # generic syntax, e.g.: `array<int, \Foo\Bar>`
|
|
|
- (?&name)+
|
|
|
- \h*<\h*
|
|
|
- (?&types)
|
|
|
- (?:
|
|
|
- \h*,\h*
|
|
|
+ (?<generic_start>
|
|
|
+ (?&name)+
|
|
|
+ \h*<\h*
|
|
|
+ )
|
|
|
+ (?<generic_types>
|
|
|
(?&types)
|
|
|
- )*
|
|
|
+ (?:
|
|
|
+ \h*,\h*
|
|
|
+ (?&types)
|
|
|
+ )*
|
|
|
+ )
|
|
|
\h*>
|
|
|
)
|
|
|
|
|
|
@@ -97,9 +106,19 @@ final class TypeExpression
|
|
|
';
|
|
|
|
|
|
/**
|
|
|
- * @var string[]
|
|
|
+ * @var string
|
|
|
+ */
|
|
|
+ private $value;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @var bool
|
|
|
+ */
|
|
|
+ private $isUnionType = false;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @var list<array{start_index: int, expression: self}>
|
|
|
*/
|
|
|
- private $types = [];
|
|
|
+ private $innerTypeExpressions = [];
|
|
|
|
|
|
/**
|
|
|
* @var null|NamespaceAnalysis
|
|
@@ -116,23 +135,16 @@ final class TypeExpression
|
|
|
*/
|
|
|
public function __construct(string $value, ?NamespaceAnalysis $namespace, array $namespaceUses)
|
|
|
{
|
|
|
- while ('' !== $value) {
|
|
|
- Preg::match(
|
|
|
- '{^'.self::REGEX_TYPES.'$}x',
|
|
|
- $value,
|
|
|
- $matches
|
|
|
- );
|
|
|
-
|
|
|
- $this->types[] = $matches['type'];
|
|
|
- $value = Preg::replace(
|
|
|
- '/^'.preg_quote($matches['type'], '/').'(\h*[|&]\h*)?/',
|
|
|
- '',
|
|
|
- $value
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
+ $this->value = $value;
|
|
|
$this->namespace = $namespace;
|
|
|
$this->namespaceUses = $namespaceUses;
|
|
|
+
|
|
|
+ $this->parse();
|
|
|
+ }
|
|
|
+
|
|
|
+ public function toString(): string
|
|
|
+ {
|
|
|
+ return $this->value;
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -140,7 +152,46 @@ final class TypeExpression
|
|
|
*/
|
|
|
public function getTypes(): array
|
|
|
{
|
|
|
- return $this->types;
|
|
|
+ if ($this->isUnionType) {
|
|
|
+ return array_map(
|
|
|
+ static function (array $type) { return $type['expression']->toString(); },
|
|
|
+ $this->innerTypeExpressions
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return [$this->value];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param callable(self $a, self $b): int $compareCallback
|
|
|
+ */
|
|
|
+ public function sortUnionTypes(callable $compareCallback): void
|
|
|
+ {
|
|
|
+ foreach (array_reverse($this->innerTypeExpressions) as [
|
|
|
+ 'start_index' => $startIndex,
|
|
|
+ 'expression' => $inner,
|
|
|
+ ]) {
|
|
|
+ $initialValueLength = \strlen($inner->toString());
|
|
|
+
|
|
|
+ $inner->sortUnionTypes($compareCallback);
|
|
|
+
|
|
|
+ $this->value = substr_replace(
|
|
|
+ $this->value,
|
|
|
+ $inner->toString(),
|
|
|
+ $startIndex,
|
|
|
+ $initialValueLength
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($this->isUnionType) {
|
|
|
+ $this->innerTypeExpressions = Utils::stableSort(
|
|
|
+ $this->innerTypeExpressions,
|
|
|
+ static function (array $type): self { return $type['expression']; },
|
|
|
+ $compareCallback
|
|
|
+ );
|
|
|
+
|
|
|
+ $this->value = implode('|', $this->getTypes());
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
public function getCommonType(): ?string
|
|
@@ -149,7 +200,7 @@ final class TypeExpression
|
|
|
|
|
|
$mainType = null;
|
|
|
|
|
|
- foreach ($this->types as $type) {
|
|
|
+ foreach ($this->getTypes() as $type) {
|
|
|
if ('null' === $type) {
|
|
|
continue;
|
|
|
}
|
|
@@ -180,7 +231,7 @@ final class TypeExpression
|
|
|
|
|
|
public function allowsNull(): bool
|
|
|
{
|
|
|
- foreach ($this->types as $type) {
|
|
|
+ foreach ($this->getTypes() as $type) {
|
|
|
if (\in_array($type, ['null', 'mixed'], true)) {
|
|
|
return true;
|
|
|
}
|
|
@@ -189,6 +240,143 @@ final class TypeExpression
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
+ private function parse(): void
|
|
|
+ {
|
|
|
+ $value = $this->value;
|
|
|
+
|
|
|
+ Preg::match(
|
|
|
+ '{^'.self::REGEX_TYPES.'$}x',
|
|
|
+ $value,
|
|
|
+ $matches
|
|
|
+ );
|
|
|
+
|
|
|
+ if ([] === $matches) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $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 = [
|