Skip to content

Commit

Permalink
bug #6063 PhpdocTypesOrderFixer - Improve nested types support (ruudk…
Browse files Browse the repository at this point in the history
…, julienfalque)

This PR was merged into the master branch.

Discussion
----------

PhpdocTypesOrderFixer - Improve nested types support

Fixes #5850, #5921 and replaces #5919. It also improves nested union types support.

Commits
-------

0c76fe5 PhpdocTypesOrderFixer - Improve nested types support
533a934 Add failing test for #5850
  • Loading branch information
SpacePossum committed Jan 20, 2022
2 parents 7d801df + 0c76fe5 commit ed38f3f
Show file tree
Hide file tree
Showing 4 changed files with 367 additions and 85 deletions.
262 changes: 225 additions & 37 deletions src/DocBlock/TypeExpression.php
Expand Up @@ -17,6 +17,7 @@
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
use PhpCsFixer\Utils;

/**
* @internal
Expand All @@ -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*>
)
|
Expand Down Expand Up @@ -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
Expand All @@ -116,31 +135,63 @@ 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;
}

/**
* @return string[]
*/
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
Expand All @@ -149,7 +200,7 @@ public function getCommonType(): ?string

$mainType = null;

foreach ($this->types as $type) {
foreach ($this->getTypes() as $type) {
if ('null' === $type) {
continue;
}
Expand Down Expand Up @@ -180,7 +231,7 @@ public function getCommonType(): ?string

public function allowsNull(): bool
{
foreach ($this->types as $type) {
foreach ($this->getTypes() as $type) {
if (\in_array($type, ['null', 'mixed'], true)) {
return true;
}
Expand All @@ -189,6 +240,143 @@ public function allowsNull(): bool
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 = [
Expand Down

0 comments on commit ed38f3f

Please sign in to comment.