Skip to content

Commit

Permalink
feat: support unions in arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
simPod committed Jul 19, 2024
1 parent d0a24b7 commit 00bfa52
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 7 deletions.
39 changes: 32 additions & 7 deletions src/Metadata/Driver/DocBlockDriver/DocBlockTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;

use function sprintf;

/**
* @internal
*/
Expand Down Expand Up @@ -114,19 +116,42 @@ private function getDocBlocTypeHint($reflector): ?string

// Generic array syntax: array<Product> | array<\Foo\Bar\Product> | array<int,Product>
if ($type instanceof GenericTypeNode) {
if ($this->isSimpleType($type->type, 'array')) {
$resolvedTypes = array_map(fn (TypeNode $node) => $this->resolveTypeFromTypeNode($node, $reflector), $type->genericTypes);
$isSimpleTypeArray = $this->isSimpleType($type->type, 'array');
$isSimpleTypeList = $this->isSimpleType($type->type, 'list');
if (!$isSimpleTypeArray && !$isSimpleTypeList) {
throw new \InvalidArgumentException(sprintf("Can't use non-array generic type %s for collection in %s:%s", (string) $type->type, $reflector->getDeclaringClass()->getName(), $reflector->getName()));
}

if ($isSimpleTypeList) {
$keyType = 'int';
$valuesIndex = 0;
} else {
if (1 === count($type->genericTypes)) {
$keyType = null;
$valuesIndex = 0;
} else {
$keyType = $this->resolveTypeFromTypeNode($type->genericTypes[0], $reflector);
$valuesIndex = 1;
}
}

return 'array<' . implode(',', $resolvedTypes) . '>';
if ($type->genericTypes[$valuesIndex] instanceof UnionTypeNode) {
$valueTypes = array_map(
fn (TypeNode $node) => $this->resolveTypeFromTypeNode($node, $reflector),
$type->genericTypes[$valuesIndex]->types,
);
} else {
$valueType = $this->resolveTypeFromTypeNode($type->genericTypes[$valuesIndex], $reflector);
$valueTypes = [$valueType];
}

if ($this->isSimpleType($type->type, 'list')) {
$resolvedTypes = array_map(fn (TypeNode $node) => $this->resolveTypeFromTypeNode($node, $reflector), $type->genericTypes);
$valueType = implode('|', $valueTypes);

return 'array<int, ' . implode(',', $resolvedTypes) . '>';
if (null === $keyType) {
return sprintf('array<%s>', $valueType);
}

throw new \InvalidArgumentException(sprintf("Can't use non-array generic type %s for collection in %s:%s", (string) $type->type, $reflector->getDeclaringClass()->getName(), $reflector->getName()));
return sprintf('array<%s, %s>', $keyType, $valueType);
}

// Primitives and class names: Collection | \Foo\Bar\Product | string
Expand Down
4 changes: 4 additions & 0 deletions src/Type/Lexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ final class Lexer extends AbstractLexer
public const T_TYPE_END = 8;
public const T_IDENTIFIER = 9;
public const T_NULL = 10;
public const T_UNION_SEPARATOR = 11;

public function parse(string $type)
{
Expand Down Expand Up @@ -103,6 +104,9 @@ protected function getType(&$value)
case '[' === $value:
return self::T_ARRAY_START;

case '|' === $value:
return self::T_UNION_SEPARATOR;

// Default
default:
// Do nothing
Expand Down
6 changes: 6 additions & 0 deletions src/Type/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ private function visitCompoundType(): array
break;
}

if ($this->lexer->isNextToken(Lexer::T_UNION_SEPARATOR)) {
$this->match(Lexer::T_UNION_SEPARATOR);

continue;
}

$this->match(Lexer::T_COMMA);
}
}
Expand Down
10 changes: 10 additions & 0 deletions tests/Fixtures/DocBlockType/Collection/MapTypedAsGenericClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,14 @@ class MapTypedAsGenericClass
* @var array<int, Product>
*/
public array $productIds;

/**
* @var array<Product|Vehicle>
*/
public array $productOrVehicleIds;

/**
* @var array<int, Product|Vehicle>
*/
public array $productOrVehicleIdsWithKey;
}
21 changes: 21 additions & 0 deletions tests/Metadata/Driver/DocBlockDriverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,27 @@ public function testInferDocBlockMapFromGenericLikeClass()
['name' => 'array', 'params' => [['name' => 'int', 'params' => []], ['name' => Product::class, 'params' => []]]],
$m->propertyMetadata['productIds']->type,
);
self::assertEquals(
[
'name' => 'array',
'params' => [
['name' => Product::class, 'params' => []],
['name' => Vehicle::class, 'params' => []],
]

Check failure on line 139 in tests/Metadata/Driver/DocBlockDriverTest.php

View workflow job for this annotation

GitHub Actions / Coding Standards (7.4)

Multi-line arrays must have a trailing comma after the last element.
],
$m->propertyMetadata['productOrVehicleIds']->type,
);
self::assertEquals(
[
'name' => 'array',
'params' => [
['name' => 'int', 'params' => []],
['name' => Product::class, 'params' => []],
['name' => Vehicle::class, 'params' => []],
]

Check failure on line 150 in tests/Metadata/Driver/DocBlockDriverTest.php

View workflow job for this annotation

GitHub Actions / Coding Standards (7.4)

Multi-line arrays must have a trailing comma after the last element.
],
$m->propertyMetadata['productOrVehicleIdsWithKey']->type,
);
}

public function testInferDocBlockCollectionOfClassesIgnoringNullTypeHint()
Expand Down

0 comments on commit 00bfa52

Please sign in to comment.