Skip to content

Commit

Permalink
Rewrote CompositeType and TypeGenerator to leverage the new union…
Browse files Browse the repository at this point in the history
…/intersection type abstractions

Signed-off-by: Marco Pivetta <ocramius@gmail.com>
  • Loading branch information
Ocramius committed Dec 8, 2022
1 parent ffbd004 commit aa62c5f
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 164 deletions.
7 changes: 4 additions & 3 deletions src/Generator/TypeGenerator.php
Expand Up @@ -5,7 +5,8 @@
use Laminas\Code\Generator\Exception\InvalidArgumentException;
use Laminas\Code\Generator\TypeGenerator\AtomicType;
use Laminas\Code\Generator\TypeGenerator\CompositeType;
use Laminas\Code\Generator\TypeGenerator\TypeInterface;
use Laminas\Code\Generator\TypeGenerator\IntersectionType;
use Laminas\Code\Generator\TypeGenerator\UnionType;
use ReflectionClass;
use ReflectionIntersectionType;
use ReflectionNamedType;
Expand Down Expand Up @@ -132,7 +133,7 @@ public static function fromTypeString(string $type): self
return new self(CompositeType::fromString($trimmedNullable));
}

private function __construct(private readonly TypeInterface $type, private readonly bool $nullable = false)
private function __construct(private readonly UnionType|IntersectionType|AtomicType $type, private readonly bool $nullable = false)
{
if ($nullable && $type instanceof AtomicType) {
$type->assertCanBeStandaloneNullable();
Expand Down Expand Up @@ -165,7 +166,7 @@ public function equals(TypeGenerator $otherType): bool
*/
public function __toString(): string
{
return $this->type->__toString();
return $this->type->toString();
}

/**
Expand Down
162 changes: 33 additions & 129 deletions src/Generator/TypeGenerator/CompositeType.php
@@ -1,159 +1,63 @@
<?php

declare(strict_types=1);

namespace Laminas\Code\Generator\TypeGenerator;

use Laminas\Code\Generator\Exception\InvalidArgumentException;

use function array_diff_key;
use function array_filter;
use function array_flip;
use function array_map;
use function assert;
use function explode;
use function implode;
use function preg_match;
use function sprintf;
use function str_contains;
use function str_ends_with;
use function str_starts_with;
use function substr;
use function usort;

/**
* Represents a union/intersection type, as supported by PHP.
* This means that this object can be composed of {@see AtomicType} or other {@see CompositeType} objects.
*
* @internal the {@see CompositeType} is an implementation detail of the type generator,
*
* @psalm-immutable
* @final
*/
final class CompositeType implements TypeInterface
abstract class CompositeType
{
public const UNION_SEPARATOR = '|';
public const INTERSECTION_SEPARATOR = '&';

/**
* @param non-empty-list<TypeInterface> $types
*/
private function __construct(protected readonly array $types, private readonly bool $isIntersection)
{
}

/** @psalm-pure */
public static function fromString(string $type): self
public static function fromString(string $type): UnionType|IntersectionType|AtomicType
{
$types = [];
$isIntersection = false;
$separator = self::UNION_SEPARATOR;

if (! str_contains($type, $separator)) {
$isIntersection = true;
$separator = self::INTERSECTION_SEPARATOR;
}

foreach (explode($separator, $type) as $typeString) {
if (str_contains($typeString, self::INTERSECTION_SEPARATOR)) {
if (! str_starts_with($typeString, '(')) {
throw new InvalidArgumentException(sprintf(
'Invalid intersection type "%s": missing opening parenthesis',
$typeString
));
}

if (! str_ends_with($typeString, ')')) {
throw new InvalidArgumentException(sprintf(
'Invalid intersection type "%s": missing closing parenthesis',
$typeString
));
}

$types[] = self::fromString(substr($typeString, 1, -1));
} else {
$types[] = AtomicType::fromString($typeString);
if (str_contains($type, self::UNION_SEPARATOR)) {
// This horrible regular expression verifies that union delimiters `|` are never contained
// in parentheses, and that all intersection `&` are contained in parentheses. It's simplistic,
// and it will crash with very large broken types, but that's sufficient for our **current**
// use-case.
// If this becomes more problematic, an actual parser is a better (although slower) alternative.
if (1 !== preg_match('/^(([|]|[^()&]+)+|(\(([&]|[^|()]+)\))+)+$/', $type)) {
throw new InvalidArgumentException(sprintf(
'Invalid type syntax "%s": intersections in a union must be surrounded by "(" and ")"',
$type
));
}
}

usort(
$types,
static function (TypeInterface $left, TypeInterface $right): int {
if ($left instanceof AtomicType && $right instanceof AtomicType) {
return [$left->sortIndex, $left->type] <=> [$right->sortIndex, $right->type];
}

return [$right instanceof self] <=> [$left instanceof self];
}
);

foreach ($types as $index => $typeItem) {
if (! $typeItem instanceof AtomicType) {
continue;
}

$otherTypes = array_diff_key($types, array_flip([$index]));

assert([] !== $otherTypes, 'There are always 2 or more types in a union type');

$otherTypes = array_filter($otherTypes, static fn (TypeInterface $type) => ! $type instanceof self);

if ([] === $otherTypes) {
continue;
}
/** @var non-empty-list<IntersectionType|AtomicType> $typesInUnion */
$typesInUnion = array_map(
self::fromString(...),
array_map(
static fn (string $type): string => trim($type, '()'),
explode(self::UNION_SEPARATOR, $type)
)
);

if ($isIntersection) {
$typeItem->assertCanIntersectWith($otherTypes);
} else {
$typeItem->assertCanUnionWith($otherTypes);
}
return new UnionType($typesInUnion);
}

if (str_contains($type, self::INTERSECTION_SEPARATOR)) {
/** @var non-empty-list<AtomicType> $typesInIntersection */
$typesInIntersection = array_map(self::fromString(...), explode(self::INTERSECTION_SEPARATOR, $type));

return new self($types, $isIntersection);
}

/**
* @return non-empty-list<TypeInterface>
*/
public function getTypes(): array
{
return $this->types;
}

public function isIntersection(): bool
{
return $this->isIntersection;
}

/** @return self::INTERSECTION_SEPARATOR|self::UNION_SEPARATOR */
public function getSeparator(): string
{
return $this->isIntersection ? self::INTERSECTION_SEPARATOR : self::UNION_SEPARATOR;
}

/** @return non-empty-string */
public function __toString(): string
{
$typesAsStrings = array_map(
static function (TypeInterface $type): string {
$typeString = $type->__toString();

return $type instanceof self && $type->isIntersection() ? sprintf('(%s)', $typeString) : $typeString;
},
$this->types
);

return implode($this->getSeparator(), $typesAsStrings);
}

/** @return non-empty-string */
public function fullyQualifiedName(): string
{
$typesAsStrings = array_map(
static function (TypeInterface $type): string {
$typeString = $type->fullyQualifiedName();

return $type instanceof self && $type->isIntersection() ? sprintf('(%s)', $typeString) : $typeString;
},
$this->types
);
return new IntersectionType($typesInIntersection);
}

return implode($this->getSeparator(), $typesAsStrings);
return AtomicType::fromString($type);
}
}
32 changes: 0 additions & 32 deletions src/Generator/TypeGenerator/TypeInterface.php

This file was deleted.

3 changes: 3 additions & 0 deletions test/Generator/TypeGenerator/CompositeTypeTest.php
@@ -1,10 +1,13 @@
<?php

declare(strict_types=1);

namespace LaminasTest\Code\Generator\TypeGenerator;

use Laminas\Code\Generator\TypeGenerator\CompositeType;
use PHPUnit\Framework\TestCase;

/** @covers \Laminas\Code\Generator\TypeGenerator\CompositeType */
class CompositeTypeTest extends TestCase
{
/**
Expand Down

0 comments on commit aa62c5f

Please sign in to comment.