Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PhpdocTypesOrderFixer - Improve nested types support #6063

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 = '
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i believe it's time (outside of this PR) to start looking for external library managing types parsing

(?<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